49.3. OAuth, Currently Oauth2

49.3.1. Whatis?

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:

  1. The application does not need to store the user's username and password.
  2. 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:

  1. The application requests permission from the user for access to protected resources.
  2. A token is issued to the application, if permission is granted by the user.
  3. The application authenticates using the token to access protected resources.

In order to use OAuth2 you must go through a couple of steps.

  • You must register your web application with one or more OAuth2 providers. Each of them will provide your application with an id, and a password. They are called clientID, and clientSecret .
  • You must then code your authentication offering your users to login to their account with you via their account with any of the OAuth2 providers you solicit.
  • It is customary that you, in addition to the OAuth2 login, offer your user the choice of registering a userid/password or email/password for login directly to you application.

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.

49.3.2. Three Examples of Login Screens with OAuth2 Providers

Figure 49.1. Gitlab
Gitlab

Figure 49.2. Bitbucket
Bitbucket

Figure 49.3. Github
Github

49.3.3. Implementation of OAuth2

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.

Example 49.1. Application Config, 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;

Example 49.2. Users Routing, 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;

Example 49.3. Controller, 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('/');
};

Example 49.4. Passport Configuration, 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');
    }
};

Example 49.5. Passport Configuration, 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);
        });
    });
};

Example 49.6. Keys, Various Constants, 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.