The protocol behind OAuth is to be read at https://tools.ietf.org/html/rfc6749. The document makes the protocol an official Internet standard. The Passport documentation has:
OAuth 2.0 (formally specified by RFC 6749) provides an authorization framework which allows users to authorize access to third-party applications. When authorized, the application is issued a token to use as an authentication credential. This has two primary security benefits:
- The application does not need to store the user's username and password.
- The token can have a restricted scope (for example: read-only access).
These benefits are particularly important for ensuring the security of web applications, making OAuth 2.0 the predominant standard for API authentication.
When using OAuth 2.0 to protect API endpoints, there are three distinct steps that must be performed:
- The application requests permission from the user for access to protected resources.
- A token is issued to the application, if permission is granted by the user.
- The application authenticates using the token to access protected resources.
In order to use OAuth2 you must go through a couple of steps.
clientID
, and clientSecret
.
Some commonly used OAuth2 providers seem to be Facebook, Google, Github, but there are plenty of others to choose from. It allows the user to use the same login to many sites using the same OAuth2 providers. The user must decide whether he or she is comfortable with potentially sharing with the chosen provider knowledge of how often he or she is using your application.
Summarily the pro for the user is having to remember but one password for many sites. The con is less privacy. Tell that to your user.
The following is an implementation of the OAuth passport strategy, and is available for cloning or download from https://bitbucket.org/phidip/passportTwo/src/master/. Choose to clone with SSH or HTTPS as befits your own setup.
code/passportTwo/app.js
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const express = require('express');
const flash = require('connect-flash');
const keys = require('./config/keys');
const logger = require('morgan');
const passport = require('passport');
const path = require('path');
const routes = require('./routes/index');
const session = require('express-session');
const Strategies = require('./config/passport')(passport);
// DB Config execute and server connect, changed to async/await form
( async function () {
try {
await mongoose.connect('mongodb://0.0.0.0/passportThree', {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
});
console.log('mongoose connection open');
} catch (err) {
console.error(err);
}
}());
const app = express();
app.locals.pretty = app.get('env') === 'development'; // pretty print html
// View engine pug and Static
app.use(express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
// Express body parser
app.use(express.urlencoded({ extended: true }));
app.use(logger('dev'));
// Express session prep
app.use(session({ // passport initialize
secret: keys.session.cookieSecret, // do the keyboard cat
resave: true, // to create entropy
saveUninitialized: false
}));
// Passport middleware prep
app.use(passport.initialize());
app.use(passport.session());
// Flash
app.use(flash());
app.use(function(req, res, next) {
res.locals.success_msg = req.flash('success_msg');
res.locals.error_msg = req.flash('error_msg');
res.locals.error = req.flash('error');
next();
});
// Routes
app.use('/', require('./routes/index.js'));
app.use('/users', require('./routes/users.js'));
// Catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// Error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
code/passportTwo/routes/users.js
const express = require('express');
const router = express.Router();
const passport = require('passport');
const auth = require("../controllers/authController.js");
const { forwardAuthenticated, ensureAuthenticated } = require('../config/auth');
router.get('/register', forwardAuthenticated, auth.register);
router.post('/register', auth.postRegister);
router.get('/login', auth.login);
// why cant this be deferred to controller?
router.get('/gitlab', passport.authenticate('gitlab', {
scope: ['email'],
passReqToCallback: true
}));
router.get('/gitlab/callback', passport.authenticate('gitlab', {
successRedirect: '/dashboard',
failureRedirect: '/users/login',
failureFlash: true
}));
// and this?
router.get('/amazon', passport.authenticate('amazon', {
scope: ['profile']
}));
router.get('/amazon/callback', passport.authenticate('amazon', {
successRedirect: '/dashboard',
failureRedirect: '/users/login',
failureFlash: true
}));
router.get('/logout', auth.logout);
router.get('/displayUsers', ensureAuthenticated, auth.readFriends);
module.exports = router;
code/passportTwo/controllers/authController.js
const bcrypt = require('bcryptjs');
const passport = require('passport');
const mongoose = require('mongoose');
const User = require('../models/User');
const saltRounds = 10;
exports.readFriends = async function (req, res) {
let us = await User.find({});
res.render('displayUsers', {
users: us,
title: 'Show friends'
});
};
exports.register = function (req, res) {
res.render('register', {
title: 'Register'
});
};
exports.postRegister = async function (req, res) {
const { userid, email } = req.body;
let errors = [];
if (!userid || !email) {
errors.push({ msg: 'Please enter all fields' });
}
if (errors.length > 0) {
res.render('register', {
errors,
userid,
email
});
}
let user = await User.findOne({ email: email });
if (user) {
errors.push({ msg: 'User already exists' });
res.render('register', {
errors,
userid,
email
});
}
const newUser = new User({name: userid, email: email});
await newUser.save();
req.flash('success_msg', 'You are now registered and can log in');
res.redirect('/users/login');
};
exports.login = function (req, res) {
res.render('login', {title: 'Login With'})
};
exports.logout = function (req, res) {
req.logout(); // passport
req.flash('success_msg', 'You are logged out');
res.redirect('/');
};
code/passportTwo/config/auth.js
module.exports = {
ensureAuthenticated: function(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
req.flash('error_msg', 'Please log in to view that resource');
res.redirect('/users/login');
},
forwardAuthenticated: function(req, res, next) {
if (!req.isAuthenticated()) {
return next();
}
res.redirect('/dashboard');
}
};
code/passportTwo/config/passport.js
const GitlabStrategy = require('passport-gitlab2');
const AmazonStrategy = require('passport-amazon');
const keys = require('./keys');
// Load User model
const User = require('../models/User');
module.exports = function (passport) {
passport.use( new GitlabStrategy( {
clientID: keys.gitlab.clientID,
clientSecret: keys.gitlab.clientSecret,
callbackURL: '/users/gitlab/callback' // url to be caught by router
},
function (accessToken, refreshToken, profile, done) {
console.log(profile);
User.findOne({email: profile._json.email})
.then(function (currentUser) {
if(currentUser) {
return done(null, currentUser);
} else {
new User({
name: profile.displayName,
email: profile._json.email
}).save()
.then(function (newUser) {
return done(null, newUser);
});
}
});
}
)
);
passport.use(new AmazonStrategy( {
clientID: keys.amazon.clientID,
clientSecret: keys.amazon.clientSecret,
callbackURL: '/users/amazon/callback' // url to be caught by router
},
function (accessToken, refreshToken, profile, done) {
User.findOne({email: profile._json.email})
.then(function (currentUser) {
if(currentUser) {
return done(null, currentUser);
} else {
new User({
name: profile.displayName,
email: profile._json.email
}).save()
.then(function (newUser) {
return done(null, newUser);
});
}
});
}
)
);
// create cookie
passport.serializeUser(function(user, done) {
done(null, user.id);
});
// find user from cookie received from server
passport.deserializeUser(function(id, done) {
User.findById(id).then(function (user) {
done(null, user);
});
});
};
code/passportTwo/config/keys.js
'use strict';
module.exports = {
gitlab: {
clientID: 'dd0c...3e09',
clientSecret: '73f7...9ae6'
},
amazon: {
clientID: 'amzn1.application-oa2-client.2a70...afa5',
clientSecret: '49f1...5907'
},
session: {
cookieSecret: 'ioer...sw£%' // keyboard cat footprints
}
};
For obvious reasons this file is not in the repo.