Skip to content

Commit

Permalink
feat(auth, config): review oAuth for google sign in ✨
Browse files Browse the repository at this point in the history
  • Loading branch information
PierreBrisorgueil committed Sep 14, 2020
1 parent dd12686 commit 4446da4
Show file tree
Hide file tree
Showing 12 changed files with 140 additions and 172 deletions.
22 changes: 11 additions & 11 deletions config/defaults/development.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ module.exports = {
googleAnalyticsTrackingID: 'WAOS_NODE_app_googleAnalyticsTrackingID',
contact: '[email protected]',
},
port: 3000,
host: 'localhost',
api: {
protocol: 'http',
port: 3000,
host: 'localhost',
base: 'api',
},
db: {
uri: 'mongodb://localhost/WaosNodeDev',
debug: true,
Expand Down Expand Up @@ -140,15 +144,11 @@ module.exports = {
},
},
},
google: {
clientId: 'WAOS_NODE_google_clientId',
},
microsoft: {
clientId: 'WAOS_NODE_microsoft_clientId',
issuer: 'https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0',
discovery: 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
// issuer: 'WAOS_NODE_microsoft_issuer',
// discovery: 'WAOS_NODE_microsoft_discovery'
oAuth: {
google: { // google console / api & service / identifier
clientID: null,
clientSecret: null,
},
},
// joi is used to manage schema restrictions, on the top of mongo / orm
joi: {
Expand Down
6 changes: 4 additions & 2 deletions config/defaults/production.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ module.exports = _.merge(defaultConfig, {
app: {
title: 'WeAreOpenSource Node - Production Environment',
},
host: '0.0.0.0',
port: 4200,
api: {
host: '0.0.0.0',
port: 4200,
},
db: {
uri: 'mongodb://localhost/WaosNode',
debug: false,
Expand Down
6 changes: 3 additions & 3 deletions lib/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ exports.bootstrap = bootstrap;
*/
const logConfiguration = () => {
// Create server URL
const server = `${(config.secure && config.secure.credentials ? 'https://' : 'http://') + config.host}:${config.port}`;
const server = `${(config.secure && config.secure.credentials ? 'https://' : 'http://') + config.api.host}:${config.api.port}`;
// Logging initialization
console.log(chalk.green(config.app.title));
console.log();
Expand All @@ -114,8 +114,8 @@ exports.start = async () => {
}

try {
if (config.secure && config.secure.credentials) http = await nodeHttps.createServer(config.secure.credentials, app).listen(config.port, config.host);
else http = await nodeHttp.createServer(app).listen(config.port, config.host);
if (config.secure && config.secure.credentials) http = await nodeHttps.createServer(config.secure.credentials, app).listen(config.api.port, config.api.host);
else http = await nodeHttp.createServer(app).listen(config.api.port, config.api.host);
logConfiguration();
return {
db,
Expand Down
48 changes: 48 additions & 0 deletions modules/users/config/strategies/google.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Module dependencies
*/
const path = require('path');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20');

const config = require(path.resolve('./config'));
const users = require('../../controllers/users.controller');

module.exports = () => {
// Use google strategy
if (config.oAuth && config.oAuth.google && config.oAuth.google.clientID && config.oAuth.google.clientSecret) {
passport.use(
new GoogleStrategy(
{
clientID: config.oAuth.google.clientID,
clientSecret: config.oAuth.google.clientSecret,
callbackURL: `${config.api.protocol}://${config.api.host}${config.api.port ? ':' : ''}${config.api.port ? config.api.port : ''}/${config.api.base}/auth/google/callback`,
scope: ['profile', 'email'],
},
async (accessToken, refreshToken, profile, cb) => {
// Set the provider data and include tokens
const providerData = profile._json;
providerData.accessToken = accessToken;
providerData.refreshToken = refreshToken;
// Create the user OAuth profile
const providerUserProfile = {
firstName: profile.name.givenName,
lastName: profile.name.familyName,
email: profile.emails[0].value,
avatar: providerData.picture ? providerData.picture : undefined,
provider: 'google',
providerIdentifierField: 'email',
providerData,
};
// Save the user OAuth profile
try {
const user = await users.saveOAuthUserProfile(providerUserProfile, 'google');
return cb(null, user);
} catch (err) {
return cb(err);
}
},
),
);
}
};
156 changes: 35 additions & 121 deletions modules/users/controllers/users/users.authentication.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@
const path = require('path');
const passport = require('passport');
const jwt = require('jsonwebtoken');
const _ = require('lodash');

const config = require(path.resolve('./config'));
const configuration = require(path.resolve('./config'));
const model = require(path.resolve('./lib/middlewares/model'));
const responses = require(path.resolve('./lib/helpers/responses'));
const errors = require(path.resolve('./lib/helpers/errors'));
const AppError = require(path.resolve('./lib/helpers/AppError'));
const UserService = require('../../services/user.service');

// URLs for which user can't be redirected on signin
const noReturnUrls = [
'/authentication/signin',
'/authentication/signup',
];
const UsersSchema = require('../../models/user.schema');

/**
* @desc Endpoint to ask the service to create a user
Expand Down Expand Up @@ -44,7 +41,7 @@ exports.signup = async (req, res) => {
*/
exports.signin = async (req, res) => {
const user = req.user;
const token = jwt.sign({ userId: user.id }, configuration.jwt.secret, { expiresIn: config.jwt.expiresIn });
const token = jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn });
return res.status(200)
.cookie('TOKEN', token, { httpOnly: true })
.json({
Expand All @@ -71,7 +68,7 @@ exports.token = async (req, res) => {
additionalProvidersData: req.user.additionalProvidersData,
};
}
const token = jwt.sign({ userId: user.id }, configuration.jwt.secret, { expiresIn: config.jwt.expiresIn });
const token = jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn });
return res.status(200)
.cookie('TOKEN', token, { httpOnly: true })
.json({ user, tokenExpiresIn: Date.now() + (config.jwt.expiresIn * 1000) });
Expand All @@ -96,15 +93,14 @@ exports.oauthCall = (req, res, next) => {
*/
exports.oauthCallback = (req, res, next) => {
const strategy = req.params.strategy;

// info.redirect_to contains intended redirect path
passport.authenticate(strategy, (err, user, { redirectTo }) => {
if (err) return res.redirect(`/authentication/signin?err=${encodeURIComponent(errors.getMessage(err))}`);
if (!user) return res.redirect('/authentication/signin');
req.login(user, (errLogin) => {
if (errLogin) return res.redirect('/authentication/signin');
return res.redirect(redirectTo || '/');
});
passport.authenticate(strategy, (err, user) => {
if (err) responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err);
else if (!user) responses.error(res, 422, 'Unprocessable Entity', errors.getMessage('could not define user in oAuth'))(err);
else {
const token = jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn });
res.cookie('TOKEN', token, { httpOnly: true });
res.redirect(302, `${config.cors.origin[0]}/token`);
}
})(req, res, next);
};

Expand All @@ -114,111 +110,29 @@ exports.oauthCallback = (req, res, next) => {
* @param {Object} providerUserProfile
* @param {Function} done - done
*/
exports.saveOAuthUserProfile = async (req, providerUserProfile, done) => {
// Setup info object
const info = {};

// Set redirection path on session.
// Do not redirect to a signin or signup page
if (noReturnUrls.indexOf(req.session.redirect_to) === -1) info.redirect_to = req.session.redirect_to;
if (!req.user) {
// Define a search query fields
const searchMainProviderIdentifierField = `providerData.${providerUserProfile.providerIdentifierField}`;
const searchAdditionalProviderIdentifierField = `additionalProvidersData.${providerUserProfile.provider}.${providerUserProfile.providerIdentifierField}`;
// Define main provider search query
const mainProviderSearchQuery = {};
mainProviderSearchQuery.provider = providerUserProfile.provider;
mainProviderSearchQuery[searchMainProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField];
// Define additional provider search query
const additionalProviderSearchQuery = {};
additionalProviderSearchQuery[searchAdditionalProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField];
// Define a search query to find existing user with current provider profile
const searchQuery = {
$or: [mainProviderSearchQuery, additionalProviderSearchQuery],
};

let user;
// check if user exist
try {
user = await UserService.search(searchQuery);
if (user) done(user, info);
} catch (err) {
done(err);
}
// if no, generate the user
try {
user = {
firstName: providerUserProfile.firstName,
lastName: providerUserProfile.lastName,
avatar: providerUserProfile.avatar,
provider: providerUserProfile.provider,
providerData: providerUserProfile.providerData,
};
// Email intentionally added later to allow defaults (sparse settings) to be applid.
// Handles case where no email is supplied.
// See comment: https://github.com/meanjs/mean/pull/1495#issuecomment-246090193
user.email = providerUserProfile.email;
} catch (err) {
done(err);
}
// save the user
try {
user = await UserService.create(user);
done(user, info);
} catch (err) {
done(err);
}
} else {
// User is already logged in, join the provider data to the existing user
let user = req.user;
// Check if user exists, is not signed in using this provider, and doesn't have that provider data already configured
if (user.provider !== providerUserProfile.provider && (!user.additionalProvidersData || !user.additionalProvidersData[providerUserProfile.provider])) {
// Add the provider data to the additional provider data field
if (!user.additionalProvidersData) {
user.additionalProvidersData = {};
}
user.additionalProvidersData[providerUserProfile.provider] = providerUserProfile.providerData;
// Then tell mongoose that we've updated the additionalProvidersData field
user.markModified('additionalProvidersData');
// And save the user
// save the user
try {
user = await UserService.create(user);
done(user, info);
} catch (err) {
done(err);
}
} else {
done(new Error('User is already connected using this provider'), user);
}
}
};

/**
* @desc Endpoint for remove oAuth provider
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
exports.removeOAuthProvider = async (req, res) => {
let user = req.user;
const provider = req.query.provider;

if (!user) return responses.error(res, 422, 'Unprocessable Entity', 'User is not authenticated')();
if (!provider) return responses.error(res, 400, 'Bad Request', 'Provider is not defined')();

// Delete the additional provider and Then tell mongoose that we've updated the additionalProvidersData field
if (user.additionalProvidersData[provider]) {
delete user.additionalProvidersData[provider];
user.markModified('additionalProvidersData');
exports.saveOAuthUserProfile = async (providerUserProfile, provider) => {
// check if user exist
try {
const user = await UserService.search({ 'providerData.email': providerUserProfile.email, provider });
if (user.length === 1) return user[0];
} catch (err) {
throw new AppError('saveOAuthUserProfile', { code: 'SERVICE_ERROR', details: err });
}

// if no, generate
try {
user = await UserService.create(user);
req.login(user, (errLogin) => {
if (errLogin) return responses.error(res, 400, 'Bad Request ', errors.getMessage(errLogin))(errLogin);
return responses.success(res, 'oAuth provider removed')(user);
});
const user = {
firstName: providerUserProfile.firstName,
lastName: providerUserProfile.lastName,
email: providerUserProfile.email,
avatar: providerUserProfile.avatar,
provider: providerUserProfile.provider,
providerData: providerUserProfile.providerData,
};
const result = model.getResultFromJoi(user, UsersSchema.User, _.clone(config.joi.validationOptions));
if (result && result.error) throw new AppError('saveOAuthUserProfile schema validation', { code: 'SERVICE_ERROR', details: result.error });
console.log('value', result.value);
return await UserService.create(result.value);
} catch (err) {
return responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err);
throw new AppError('saveOAuthUserProfile', { code: 'SERVICE_ERROR', details: err });
}
};
7 changes: 3 additions & 4 deletions modules/users/controllers/users/users.password.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const UserService = require('../../services/user.service');
const mails = require(path.resolve('./lib/helpers/mails'));
const errors = require(path.resolve('./lib/helpers/errors'));
const responses = require(path.resolve('./lib/helpers/responses'));
const configuration = require(path.resolve('./config'));
const config = require(path.resolve('./config'));

/**
Expand All @@ -29,7 +28,7 @@ exports.forgot = async (req, res) => {
if (user.provider !== 'local') return responses.error(res, 400, 'Bad Request', `It seems like you signed up using your ${user.provider} account`)();

const edit = {
resetPasswordToken: jwt.sign({ exp: Date.now() + 3600000 }, configuration.jwt.secret, { algorithm: 'HS256' }),
resetPasswordToken: jwt.sign({ exp: Date.now() + 3600000 }, config.jwt.secret, { algorithm: 'HS256' }),
resetPasswordExpires: Date.now() + 3600000,
};
user = await UserService.update(user, edit, 'recover');
Expand Down Expand Up @@ -89,7 +88,7 @@ exports.reset = async (req, res) => {
};
user = await UserService.update(user, edit, 'recover');
return res.status(200)
.cookie('TOKEN', jwt.sign({ userId: user.id }, configuration.jwt.secret, { expiresIn: config.jwt.expiresIn }), { httpOnly: true })
.cookie('TOKEN', jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn }), { httpOnly: true })
.json({
user, tokenExpiresIn: Date.now() + (config.jwt.expiresIn * 1000), type: 'sucess', message: 'Password changed successfully',
});
Expand Down Expand Up @@ -129,7 +128,7 @@ exports.updatePassword = async (req, res) => {
password = UserService.checkPassword(req.body.newPassword);
user = await UserService.update(user, { password }, 'recover');
return res.status(200)
.cookie('TOKEN', jwt.sign({ userId: user.id }, configuration.jwt.secret, { expiresIn: config.jwt.expiresIn }), { httpOnly: true })
.cookie('TOKEN', jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn }), { httpOnly: true })
.json({
user, tokenExpiresIn: Date.now() + (config.jwt.expiresIn * 1000), type: 'sucess', message: 'Password changed successfully',
});
Expand Down
24 changes: 0 additions & 24 deletions modules/users/controllers/users/users.profile.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@
* Module dependencies
*/
const path = require('path');
const jwt = require('jsonwebtoken');

const errors = require(path.resolve('./lib/helpers/errors'));
const responses = require(path.resolve('./lib/helpers/responses'));
const config = require(path.resolve('./config'));
const UserService = require('../../services/user.service');
const oAuthService = require('../../services/user.service');

/**
* @desc Endpoint to ask the service to update a user
Expand Down Expand Up @@ -67,24 +64,3 @@ exports.me = (req, res) => {
}
return responses.success(res, 'user get')(user);
};

/**
* @desc Endpoint to add oAuthProvider
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
exports.addOAuthProviderUserProfile = async (req, res) => {
let user;
try {
user = await oAuthService.addUser(req.body.provider, req.body.idToken);
} catch (err) {
return responses.error(res, 304, 'Not Modified', errors.getMessage(err))(err);
}
if (!user) return responses.error(res, 404, 'Not Found', 'No Oauth found')();

const token = jwt.sign({ userId: user.id }, config.jwt.secret, { expiresIn: config.jwt.expiresIn });

res.status(200)
.cookie('TOKEN', token, { httpOnly: true })
.json({ user, tokenExpiresIn: Date.now() + (config.jwt.expiresIn * 1000) });
};
1 change: 0 additions & 1 deletion modules/users/models/user.model.mongoose.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ mongoose.Promise = Promise;
* User Schema
*/
const UserMongoose = new Schema({
sub: String,
firstName: String,
lastName: String,
bio: String,
Expand Down
Loading

0 comments on commit 4446da4

Please sign in to comment.