1 – server/app.js
const express = require("express");
const morgan = require("morgan");
const mongoose = require("mongoose");
const cors = require("cors");
const cookieParser = require("cookie-parser");
mongoose.Promise = global.Promise;
if (process.env.NODE_ENV === "test") {
mongoose.connect("mongodb://localhost/APIAuthenticationTEST", {
useNewUrlParser: true
});
} else {
mongoose.connect("mongodb://localhost/APIAuthentication", {
useNewUrlParser: true
});
}
const app = express();
app.use(cookieParser());
app.use(
cors({
origin: "http://localhost:3000",
credentials: true
})
);
// Middlewares moved morgan into if for clear tests
if (!process.env.NODE_ENV === "test") {
app.use(morgan("dev"));
}
app.use(express.json());
// Routes
app.use("/users", require("./routes/users"));
module.exports = app;
2-server/index.js
const app = require('./app');
// Start the server
const port = process.env.PORT || 5000;
app.listen(port);
console.log(`Server listening at ${port}`);
3 – helpers/routeHelpers.js
const Joi = require('joi');
module.exports = {
validateBody: (schema) => {
return (req, res, next) => {
const result = Joi.validate(req.body, schema);
if (result.error) {
return res.status(400).json(result.error);
}
if (!req.value) { req.value = {}; }
req.value['body'] = result.value;
next();
}
},
schemas: {
authSchema: Joi.object().keys({
email: Joi.string().email().required(),
password: Joi.string().required()
})
}
}
routes/users.js
const express = require('express');
const router = require('express-promise-router')();
const passport = require('passport');
const passportConf = require('../passport');
const { validateBody, schemas } = require('../helpers/routeHelpers');
const UsersController = require('../controllers/users');
const passportSignIn = passport.authenticate('local', { session: false });
const passportJWT = passport.authenticate('jwt', { session: false });
router.route('/signup')
.post(validateBody(schemas.authSchema), UsersController.signUp);
router.route('/signin')
.post(validateBody(schemas.authSchema), passportSignIn, UsersController.signIn);
router.route('/signout')
.get(passportJWT, UsersController.signOut);
router.route('/oauth/google')
.post(passport.authenticate('googleToken', { session: false }), UsersController.googleOAuth);
router.route('/oauth/facebook')
.post(passport.authenticate('facebookToken', { session: false }), UsersController.facebookOAuth);
router.route('/oauth/link/google')
.post(passportJWT, passport.authorize('googleToken', { session: false }), UsersController.linkGoogle)
router.route('/oauth/unlink/google')
.post(passportJWT, UsersController.unlinkGoogle);
router.route('/oauth/link/facebook')
.post(passportJWT, passport.authorize('facebookToken', { session: false }), UsersController.linkFacebook)
router.route('/oauth/unlink/facebook')
.post(passportJWT, UsersController.unlinkFacebook);
router.route('/dashboard')
.get(passportJWT, UsersController.dashboard);
router.route('/status')
.get(passportJWT, UsersController.checkAuth);
module.exports = router;
4 – models/user.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const Schema = mongoose.Schema;
// Create a schema
const userSchema = new Schema({
methods: {
type: [String],
required: true
},
local: {
email: {
type: String,
lowercase: true
},
password: {
type: String
}
},
google: {
id: {
type: String
},
email: {
type: String,
lowercase: true
}
},
facebook: {
id: {
type: String
},
email: {
type: String,
lowercase: true
}
}
});
userSchema.pre('save', async function (next) {
try {
console.log('entered');
if (!this.methods.includes('local')) {
next();
}
//the user schema is instantiated
const user = this;
//check if the user has been modified to know if the password has already been hashed
if (!user.isModified('local.password')) {
next();
}
// Generate a salt
const salt = await bcrypt.genSalt(10);
// Generate a password hash (salt + hash)
const passwordHash = await bcrypt.hash(this.local.password, salt);
// Re-assign hashed version over original, plain text password
this.local.password = passwordHash;
console.log('exited');
next();
} catch (error) {
next(error);
}
});
userSchema.methods.isValidPassword = async function (newPassword) {
try {
return await bcrypt.compare(newPassword, this.local.password);
} catch (error) {
throw new Error(error);
}
}
// Create a model
const User = mongoose.model('user', userSchema);
// Export the model
module.exports = User;
5 – configuration/index.js
if (process.env.NODE_ENV === 'test') {
module.exports = {
JWT_SECRET: 'codeworkrauthentication',
oauth: {
google: {
clientID: 'number',
clientSecret: 'string',
},
facebook: {
clientID: 'number',
clientSecret: 'string',
},
},
};
} else {
module.exports = {
JWT_SECRET: 'codeworkrauthentication',
oauth: {
google: {
clientID: 'number',
clientSecret: 'string',
},
facebook: {
clientID: 'number',
clientSecret: 'string',
},
},
};
}
6 – controllers/users.js
const JWT = require('jsonwebtoken');
const User = require('../models/user');
const { JWT_SECRET } = require('../configuration');
signToken = user => {
return JWT.sign({
iss: 'CodeWorkr',
sub: user.id,
iat: new Date().getTime(), // current time
exp: new Date().setDate(new Date().getDate() + 1) // current time + 1 day ahead
}, JWT_SECRET);
}
module.exports = {
signUp: async (req, res, next) => {
const { email, password } = req.value.body;
// Check if there is a user with the same email
let foundUser = await User.findOne({ "local.email": email });
if (foundUser) {
return res.status(403).json({ error: 'Email is already in use'});
}
// Is there a Google account with the same email?
foundUser = await User.findOne({
$or: [
{ "google.email": email },
{ "facebook.email": email },
]
});
if (foundUser) {
// Let's merge them?
foundUser.methods.push('local')
foundUser.local = {
email: email,
password: password
}
await foundUser.save()
// Generate the token
const token = signToken(foundUser);
// Respond with token
res.cookie('access_token', token, {
httpOnly: true
});
res.status(200).json({ success: true });
}
// Is there a Google account with the same email?
// foundUser = await User.findOne({ "facebook.email": email });
// if (foundUser) {
// // Let's merge them?
// foundUser.methods.push('local')
// foundUser.local = {
// email: email,
// password: password
// }
// await foundUser.save()
// // Generate the token
// const token = signToken(foundUser);
// // Respond with token
// res.status(200).json({ token });
// }
// Create a new user
const newUser = new User({
methods: ['local'],
local: {
email: email,
password: password
}
});
await newUser.save();
// Generate the token
const token = signToken(newUser);
// Send a cookie containing JWT
res.cookie('access_token', token, {
httpOnly: true
});
res.status(200).json({ success: true });
},
signIn: async (req, res, next) => {
// Generate token
const token = signToken(req.user);
res.cookie('access_token', token, {
httpOnly: true
});
res.status(200).json({ success: true });
},
signOut: async (req, res, next) => {
res.clearCookie('access_token');
// console.log('I managed to get here!');
res.json({ success: true });
},
googleOAuth: async (req, res, next) => {
// Generate token
const token = signToken(req.user);
res.cookie('access_token', token, {
httpOnly: true
});
res.status(200).json({ success: true });
},
linkGoogle: async (req, res, next) => {
res.json({
success: true,
methods: req.user.methods,
message: 'Successfully linked account with Google'
});
},
unlinkGoogle: async (req, res, next) => {
// Delete Google sub-object
if (req.user.google) {
req.user.google = undefined
}
// Remove 'google' from methods array
const googleStrPos = req.user.methods.indexOf('google')
if (googleStrPos >= 0) {
req.user.methods.splice(googleStrPos, 1)
}
await req.user.save()
// Return something?
res.json({
success: true,
methods: req.user.methods,
message: 'Successfully unlinked account from Google'
});
},
facebookOAuth: async (req, res, next) => {
// Generate token
const token = signToken(req.user);
res.cookie('access_token', token, {
httpOnly: true
});
res.status(200).json({ success: true });
},
linkFacebook: async (req, res, next) => {
res.json({
success: true,
methods: req.user.methods,
message: 'Successfully linked account with Facebook'
});
},
unlinkFacebook: async (req, res, next) => {
// Delete Facebook sub-object
if (req.user.facebook) {
req.user.facebook = undefined
}
// Remove 'facebook' from methods array
const facebookStrPos = req.user.methods.indexOf('facebook')
if (facebookStrPos >= 0) {
req.user.methods.splice(facebookStrPos, 1)
}
await req.user.save()
// Return something?
res.json({
success: true,
methods: req.user.methods,
message: 'Successfully unlinked account from Facebook'
});
},
dashboard: async (req, res, next) => {
console.log('I managed to get here!');
res.json({
secret: "resource",
methods: req.user.methods
});
},
checkAuth: async (req, res, next) => {
console.log('I managed to get here!');
res.json({ success: true });
}
}
8 – passport.js
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const { ExtractJwt } = require('passport-jwt');
const LocalStrategy = require('passport-local').Strategy;
const GooglePlusTokenStrategy = require('passport-google-plus-token');
const FacebookTokenStrategy = require('passport-facebook-token');
const config = require('./configuration');
const User = require('./models/user');
const cookieExtractor = req => {
let token = null;
if (req && req.cookies) {
token = req.cookies['access_token'];
}
return token;
}
// JSON WEB TOKENS STRATEGY
passport.use(new JwtStrategy({
jwtFromRequest: cookieExtractor,
secretOrKey: config.JWT_SECRET,
passReqToCallback: true
}, async (req, payload, done) => {
try {
// Find the user specified in token
const user = await User.findById(payload.sub);
// If user doesn't exists, handle it
if (!user) {
return done(null, false);
}
// Otherwise, return the user
req.user = user;
done(null, user);
} catch(error) {
done(error, false);
}
}));
// LOCAL STRATEGY
passport.use(new LocalStrategy({
usernameField: 'email'
}, async (email, password, done) => {
try {
// Find the user given the email
const user = await User.findOne({ "local.email": email });
// If not, handle it
if (!user) {
return done(null, false);
}
// Check if the password is correct
const isMatch = await user.isValidPassword(password);
// If not, handle it
if (!isMatch) {
return done(null, false);
}
// Otherwise, return the user
done(null, user);
} catch(error) {
done(error, false);
}
}));
// Google OAuth Strategy
passport.use('googleToken', new GooglePlusTokenStrategy({
clientID: config.oauth.google.clientID,
clientSecret: config.oauth.google.clientSecret,
passReqToCallback: true,
}, async (req, accessToken, refreshToken, profile, done) => {
try {
// Could get accessed in two ways:
// 1) When registering for the first time
// 2) When linking account to the existing one
// Should have full user profile over here
console.log('profile', profile);
console.log('accessToken', accessToken);
console.log('refreshToken', refreshToken);
if (req.user) {
// We're already logged in, time for linking account!
// Add Google's data to an existing account
req.user.methods.push('google')
req.user.google = {
id: profile.id,
email: profile.emails[0].value
}
await req.user.save()
return done(null, req.user);
} else {
// We're in the account creation process
let existingUser = await User.findOne({ "google.id": profile.id });
if (existingUser) {
return done(null, existingUser);
}
// Check if we have someone with the same email
existingUser = await User.findOne({ "local.email": profile.emails[0].value })
if (existingUser) {
// We want to merge google's data with local auth
existingUser.methods.push('google')
existingUser.google = {
id: profile.id,
email: profile.emails[0].value
}
await existingUser.save()
return done(null, existingUser);
}
const newUser = new User({
methods: ['google'],
google: {
id: profile.id,
email: profile.emails[0].value
}
});
await newUser.save();
done(null, newUser);
}
} catch(error) {
done(error, false, error.message);
}
}));
passport.use('facebookToken', new FacebookTokenStrategy({
clientID: config.oauth.facebook.clientID,
clientSecret: config.oauth.facebook.clientSecret,
passReqToCallback: true
}, async (req, accessToken, refreshToken, profile, done) => {
try {
console.log('profile', profile);
console.log('accessToken', accessToken);
console.log('refreshToken', refreshToken);
if (req.user) {
// We're already logged in, time for linking account!
// Add Facebook's data to an existing account
req.user.methods.push('facebook')
req.user.facebook = {
id: profile.id,
email: profile.emails[0].value
}
await req.user.save();
return done(null, req.user);
} else {
// We're in the account creation process
let existingUser = await User.findOne({ "facebook.id": profile.id });
if (existingUser) {
return done(null, existingUser);
}
// Check if we have someone with the same email
existingUser = await User.findOne({ "local.email": profile.emails[0].value })
if (existingUser) {
// We want to merge facebook's data with local auth
existingUser.methods.push('facebook')
existingUser.facebook = {
id: profile.id,
email: profile.emails[0].value
}
await existingUser.save()
return done(null, existingUser);
}
const newUser = new User({
methods: ['facebook'],
facebook: {
id: profile.id,
email: profile.emails[0].value
}
});
await newUser.save();
done(null, newUser);
}
} catch(error) {
done(error, false, error.message);
}
}));
Reference
APIAuthenticationWithNode
Playlist
Google OAuth 2.0 Integration with NodeJS (Without Passport)