OAuth, Currently Oauth2

Whatis?

Time constraints precludes diving into the OAuth strategy with adapted code examples as we have just done with OpenID. The Passport website has documentation with important code examples at http://www.passportjs.org/docs/oauth/. You may also study the protocol behind it at https://tools.ietf.org/html/rfc6749. The latter document means that the protocol is 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.

Overview

At this point we have not yet created out own material. There is however, an excellent video tutorial available on Youtube. Please refer to Net Ninja's OAuth with Passport. In it he creates manually an Express application very similar to what we get from using the Express CLI tool:

express --view=pug myapp

except of course, he uses ejs in the tutorial.

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.

The most commonly used OAuth2 providers seem to be Facebook, Google, Github but there are plenty 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.

Three Examples of Login Screens with OAuth2 Providers

Figure 49.1. Gitlab
Gitlab

Figure 49.2. Bitbucket
Bitbucket

Figure 49.3. Github
Github

Notice that Github doesn't share ;) Neither does Facebook and Google by the way.

Based on the Net Ninja's OAuth with Passporttutorial, we have built an example using two providers: Gitlab, and Amazon. Both are, of course supported by Passport. Some providers are well, overpdeantic, in what and how they ask you to register your application. Gitlab and Amazon chosen here, are easy to satisfy.

Howto?

In this example we have not built in the additional local strategy. Therefore there is no local login form. The login page the user is faced with consequently just contains links to the servicing OAuth2 providers.

Example 49.1. Login Page for OpenID Authentication
extends layout

block content
    h1 Login
    if errors != undefined && errors.length != 0
        each error in errors
            div(role='alert' id='a1') #{error.msg}
    if success_msg != ''
        div(role='alert' id='a2') #{success_msg}
    if error_msg != ''
        div(role='alert' id='a3') #{error_msg}
    if error != ''
        div(role='alert' id='a4') #{error}
    ul.nav
        li
            a(href="/users/gitlab") Log In with Gitlab
        li
            a(href="/users/amazon") Log In with Amazon

Routing requires two routes apart from displaying the login page. One route takes the user to the OpenID verification site. Here the actual entry of credentials takes place. Return is to the login url with an /return suffixed.

Example 49.2. Routing with OAuth2
const express = require('express');
const router = express.Router();
const passport = require('passport');
const auth = require("../controllers/authController.js");
const { forwardAuthenticated } = 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);

module.exports = router;

Our ususal coding style would require deferring controller action to do the actual activity. Tests showed that this did not have the desired results, therefore the invoking of passport.authenticate(strategy) is done directly from the router. The same goes for the second invokation of that function.

Why is the passport.authenticate(strategy) called twice? The way OAuth2 works, the authentication step, the first call, just returns a code. The callback, the second call, then sends the code to the provider, and if approved, gets the result corresponding to the desired scope.

This seems really simple, too simple perhaps, and it is. The essentials plays out in the configuration of the strategies.

The Passport documentation of the configuration of the strategies is a bit terse. It really must look something akin to

Example 49.3. OAuth2 Strategy Configuration File, 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'
        },
        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'
        },
        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);
        });
    });
};

Notice of course that the example offers two providers.

In real life cases it might be useful to put profile info into the user objects. Is the user an administrator, a regular user, or even a more fine grained profile. The passport documentation has some remarks on the possibility of just that. In the code above you will see that if a user logs in, and we do not have that user in our own database, he/she will be registered there.