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:
- 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.
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.
clientID, and clientSecret
.
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.
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.
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.
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.
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
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.