Skip to content

Add local authentication strategy and user registration #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 355 additions & 6 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"license": "ISC",
"dependencies": {
"@octokit/rest": "^20.1.1",
"bcrypt": "^5.1.1",
"compression": "^1.7.4",
"connect-mongo": "^5.1.0",
"cookie-parser": "^1.4.6",
Expand All @@ -42,6 +43,7 @@
"octokit": "^3.2.1",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-local": "^1.0.0",
"pm2": "^5.3.1",
"short-uuid": "^5.2.0",
"ulid": "^2.3.0",
Expand Down
109 changes: 109 additions & 0 deletions src/auth/githubStrategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const config = require('../configs');
const GitHubStrategy = require('passport-github2').Strategy;

const { encryptToken } = require('./util');
const {
getByGitHubId,
create,
updateById,
} = require('../domains/user/service');
const { AppError } = require('../libraries/error-handling/AppError');

const getGitHubStrategy = () => {
return new GitHubStrategy(
{
clientID: config.GITHUB_CLIENT_ID,
clientSecret: config.GITHUB_CLIENT_SECRET,
callbackURL: `${config.HOST}/api/auth/github/callback`,
},
async (accessToken, refreshToken, profile, cb) => {
try {
const trimmedPayloadForSession = await getOrCreateUserFromGitHubProfile(
{
profile,
accessToken,
}
);

cb(null, trimmedPayloadForSession); // Pass the user object to the session
} catch (error) {
cb(error, null);
}
}
);
};

async function getOrCreateUserFromGitHubProfile({ profile, accessToken }) {
const isAdmin = config.ADMIN_USERNAMES.includes(profile.username);
// Create a new user from GitHub API Profile data
const payload = {
githubId: profile.id,
nodeId: profile.nodeId,
displayName: profile.displayName,
username: profile.username,
profileUrl: profile.profileUrl,

avatarUrl: profile._json.avatar_url,
apiUrl: profile._json.url,
company: profile._json.company,
blog: profile._json.blog,
location: profile._json.location,
email: profile._json.email,
hireable: profile._json.hireable,
bio: profile._json.bio,
public_repos: profile._json.public_repos,
public_gists: profile._json.public_gists,
followers: profile._json.followers,
following: profile._json.following,
created_at: profile._json.created_at,
updated_at: profile._json.updated_at,

isDemo: false,
isVerified: true,
isAdmin,
};

let user = await getByGitHubId(profile.id);

const tokenInfo = encryptToken(accessToken);
if (user) {
if (user.isDeactivated) {
throw new AppError('user-is-deactivated', 'User is deactivated', 401);
}

// Update the user with the latest data
user = Object.assign(user, payload, {
accessToken: tokenInfo.token,
accessTokenIV: tokenInfo.iv,
updatedAt: new Date(),
});
await updateById(user._id, user);
} else {
// Create a new user
user = await create({
...payload,
accessToken: tokenInfo.token,
accessTokenIV: tokenInfo.iv,
});
}
const userObj = user.toObject();
const trimmedPayloadForSession = {
_id: userObj._id,
githubId: userObj.githubId,
nodeId: userObj.nodeId,
isAdmin: userObj.isAdmin,
isDeactivated: userObj.isDeactivated,
isDemo: userObj.isDemo,
// UI info
username: userObj.username,
displayName: userObj.displayName,
avatarUrl: userObj.avatarUrl,
email: userObj.email,
};
return trimmedPayloadForSession;
}

module.exports = {
getGitHubStrategy,
getOrCreateUserFromGitHubProfile,
};
138 changes: 9 additions & 129 deletions src/auth/index.js
Original file line number Diff line number Diff line change
@@ -1,133 +1,11 @@
const config = require('../configs');
const crypto = require('crypto');
const logger = require('../libraries/log/logger');
const GitHubStrategy = require('passport-github2').Strategy;
const { encryptToken, decryptToken } = require('./util');
const { updateById } = require('../domains/user/service');

const {
getByGitHubId,
create,
updateById,
} = require('../domains/user/service');
const { AppError } = require('../libraries/error-handling/AppError');

function encryptToken(token) {
const encryptionKey = config.ENCRYPTION_KEY;
const iv = crypto.randomBytes(16); // Generate a secure IV
const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv);
let encrypted = cipher.update(token, 'utf-8', 'hex');
encrypted += cipher.final('hex');

return {
token: encrypted,
iv: iv.toString('hex'),
};
}

function decryptToken(encryptedToken, iv) {
const encryptionKey = config.ENCRYPTION_KEY;
const decipher = crypto.createDecipheriv(
'aes-256-cbc',
encryptionKey,
Buffer.from(iv, 'hex')
);
let decrypted = decipher.update(encryptedToken, 'hex', 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
}

async function getOrCreateUserFromGitHubProfile({ profile, accessToken }) {
const isAdmin = config.ADMIN_USERNAMES.includes(profile.username);
// Create a new user from GitHub API Profile data
const payload = {
githubId: profile.id,
nodeId: profile.nodeId,
displayName: profile.displayName,
username: profile.username,
profileUrl: profile.profileUrl,

avatarUrl: profile._json.avatar_url,
apiUrl: profile._json.url,
company: profile._json.company,
blog: profile._json.blog,
location: profile._json.location,
email: profile._json.email,
hireable: profile._json.hireable,
bio: profile._json.bio,
public_repos: profile._json.public_repos,
public_gists: profile._json.public_gists,
followers: profile._json.followers,
following: profile._json.following,
created_at: profile._json.created_at,
updated_at: profile._json.updated_at,

isDemo: false,
isVerified: true,
isAdmin,
};

let user = await getByGitHubId(profile.id);

const tokenInfo = encryptToken(accessToken);
if (user) {
if (user.isDeactivated) {
throw new AppError('user-is-deactivated', 'User is deactivated', 401);
}

// Update the user with the latest data
user = Object.assign(user, payload, {
accessToken: tokenInfo.token,
accessTokenIV: tokenInfo.iv,
updatedAt: new Date(),
});
await updateById(user._id, user);
} else {
// Create a new user
user = await create({
...payload,
accessToken: tokenInfo.token,
accessTokenIV: tokenInfo.iv,
});
}
const userObj = user.toObject();
const trimmedPayloadForSession = {
_id: userObj._id,
githubId: userObj.githubId,
nodeId: userObj.nodeId,
isAdmin: userObj.isAdmin,
isDeactivated: userObj.isDeactivated,
isDemo: userObj.isDemo,
// UI info
username: userObj.username,
displayName: userObj.displayName,
avatarUrl: userObj.avatarUrl,
email: userObj.email,
};
return trimmedPayloadForSession;
}

const getGitHubStrategy = () => {
return new GitHubStrategy(
{
clientID: config.GITHUB_CLIENT_ID,
clientSecret: config.GITHUB_CLIENT_SECRET,
callbackURL: `${config.HOST}/api/auth/github/callback`,
},
async (accessToken, refreshToken, profile, cb) => {
try {
const trimmedPayloadForSession = await getOrCreateUserFromGitHubProfile(
{
profile,
accessToken,
}
);

cb(null, trimmedPayloadForSession); // Pass the user object to the session
} catch (error) {
cb(error, null);
}
}
);
};
getGitHubStrategy,
getOrCreateUserFromGitHubProfile,
} = require('./githubStrategy');
const { localStrategy, registerUser } = require('./localStrategy');

// clear the accessToken value from database after logout
const clearAuthInfo = async (userId) => {
Expand All @@ -139,9 +17,11 @@ const clearAuthInfo = async (userId) => {
};

module.exports = {
getOrCreateUserFromGitHubProfile,
getGitHubStrategy,
getOrCreateUserFromGitHubProfile,
clearAuthInfo,
encryptToken,
decryptToken,
localStrategy,
registerUser,
};
80 changes: 80 additions & 0 deletions src/auth/localStrategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const { getByUsername, getByEmail, create } = require('../domains/user/service');
const { AppError } = require('../libraries/error-handling/AppError');

const verifyCallback = async (username, password, done) => {
try {
const user = await getByUsername(username);
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
console.log('user', user);
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const isValidPassword = await bcrypt.compare(password, user.password);

console.log(
'user',
password,
user.password,
hashedPassword,
isValidPassword
);
if (!isValidPassword) {
return done(null, false, { message: 'Incorrect password.' });
}

return done(null, user);
} catch (err) {
return done(err);
}
};

const registerUser = async ({ email, password }) => {
try {
console.log('registerUser', email, password);
// Check if user already exists
const existingUser = await getByEmail(email);
if (existingUser) {
throw new AppError('user-already-exists', 'Email already taken', 400);
}

// Hash the password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);

// Create user payload
const payload = {
email,
username: email,
password: hashedPassword,
displayName: email,
isDemo: false,
isVerified: false,
isAdmin: false,
};

// Create the user
const newUser = await create(payload);

// Prepare the user object for the session
const userObj = newUser.toObject();
const trimmedPayloadForSession = {
_id: userObj._id,
isAdmin: userObj.isAdmin,
isDeactivated: userObj.isDeactivated,
isDemo: userObj.isDemo,
username: userObj.username,
displayName: userObj.displayName,
};

return trimmedPayloadForSession;
} catch (error) {
throw new AppError('registration-failed', error.message, 400);
}
};

const localStrategy = new LocalStrategy(verifyCallback);

module.exports = { localStrategy, registerUser };
32 changes: 32 additions & 0 deletions src/auth/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const config = require('../configs');
const crypto = require('crypto');

function encryptToken(token) {
const encryptionKey = config.ENCRYPTION_KEY;
const iv = crypto.randomBytes(16); // Generate a secure IV
const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv);
let encrypted = cipher.update(token, 'utf-8', 'hex');
encrypted += cipher.final('hex');

return {
token: encrypted,
iv: iv.toString('hex'),
};
}

function decryptToken(encryptedToken, iv) {
const encryptionKey = config.ENCRYPTION_KEY;
const decipher = crypto.createDecipheriv(
'aes-256-cbc',
encryptionKey,
Buffer.from(iv, 'hex')
);
let decrypted = decipher.update(encryptedToken, 'hex', 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
}

module.exports = {
encryptToken,
decryptToken,
};
Loading