UNPKG

@inspire-platform/sails-hook-auth

Version:

Passport-based User Authentication system for sails.js applications.

360 lines (325 loc) 12.9 kB
'use strict'; if (sails.services.passport) { module.exports = sails.services.passport; } else { var path = require('path'); var url = require('url'); var passport = require('passport'); var _ = require('lodash'); /** * Passport Service * * A painless Passport.js service for your Sails app that is guaranteed to * Rock Your Socks. It takes all the hassle out of setting up Passport.js by * encapsulating all the boring stuff in two functions: * * passport.endpoint() * passport.callback() * * The former sets up an endpoint (/auth/:provider) for redirecting a user to a * third-party provider for authentication, while the latter sets up a callback * endpoint (/auth/:provider/callback) for receiving the response from the * third-party provider. All you have to do is define in the configuration which * third-party providers you'd like to support. It's that easy! * * Behind the scenes, the service stores all the data it needs within "Pass- * ports". These contain all the information required to associate a local user * with a profile from a third-party provider. This even holds true for the good * ol' password authentication scheme – the Authentication Service takes care of * encrypting passwords and storing them in Passports, allowing you to keep your * User model free of bloat. */ // Load authentication protocols passport.protocols = require('./protocols'); /** * Connect a third-party profile to a local user * * This is where most of the magic happens when a user is authenticating with a * third-party provider. What it does, is the following: * * 1. Given a provider and an identifier, find a matching Passport. * 2. From here, the logic branches into two paths. * * - A user is not currently logged in: * 1. If a Passport wassn't found, create a new user as well as a new * Passport that will be assigned to the user. * 2. If a Passport was found, get the user associated with the passport. * * - A user is currently logged in: * 1. If a Passport wasn't found, create a new Passport and associate it * with the already logged in user (ie. "Connect") * 2. If a Passport was found, nothing needs to happen. * * As you can see, this function handles both "authentication" and "authori- * zation" at the same time. This is due to the fact that we pass in * `passReqToCallback: true` when loading the strategies, allowing us to look * for an existing session in the request and taking action based on that. * * For more information on auth(entication|rization) in Passport.js, check out: * http://passportjs.org/guide/authenticate/ * http://passportjs.org/guide/authorize/ * * @param {Object} req * @param {Object} query * @param {Object} profile * @param {Function} next */ passport.connect = function (req, query, profile, next) { var user = {}; // Use profile.provider or fallback to the query.provider if it is undefined // as is the case for OpenID, for example var provider = profile.provider || req.param('provider'); req.session.tokens = query.tokens; // Get the authentication provider from the query. query.provider = provider; // If the provider cannot be identified we cannot match it to a passport so // throw an error and let whoever's next in line take care of it. if (!provider) { return next(new Error('No authentication provider was identified.')); } sails.log.debug('auth profile', profile); // If the profile object contains a list of emails, grab the first one and // add it to the user. if (profile.emails && profile.emails[0]) { user.email = profile.emails[0].value; } // If the profile object contains a username, add it to the user. if (_.has(profile, 'username')) { user.username = profile.username; } // If neither an email or a username was available in the profile, we don't // have a way of identifying the user in the future. Throw an error and let // whoever's next in the line take care of it. if (!user.username && !user.email) { return next(new Error('Neither a username nor email was available')); } sails.models.passport.findOne({ provider: provider, identifier: query.identifier.toString() }).then(function (passport) { if (!req.user) { // Scenario: A new user is attempting to sign up using a third-party // authentication provider. // Action: Create a new user and assign them a passport. if (!passport) { return sails.models.user.create(user).meta({ fetch: true }).then(function (_user) { user = _user; return sails.models.passport.create(_.extend({ user: user.id }, query)); }).then(function () { next(null, user); }).catch(next); } // Scenario: An existing user is trying to log in using an already // connected passport. // Action: Get the user associated with the passport. else { // maybe update passport return new Promise(function (resolve, reject) { // have tokens changed since last login? if (_.has(query, 'tokens') && query.tokens != passport.tokens) { // yes, update them return sails.models.passport.update(passport.id, { tokens: query.tokens }).meta({ fetch: true }).then(function (result) { if (_.isArray(result) && result.length > 0) { return resolve(result[0]); } else { return reject(new Error('Something went terribly wrong while update passport tokens.')); } }).catch(reject); } else { return resolve(passport); } }).then(function (passport) { // Fetch the user associated with the Passport return sails.services.authservice.findUser({ id: passport.user }); }).then(function (user) { // all done return next(null, user); }).catch(next); } } else { // Scenario: A user is currently logged in and trying to connect a new // passport. // Action: Create and assign a new passport to the user. if (!passport) { return sails.models.passport.create(_.extend({ user: req.user.id }, query)).then(function () { next(null, req.user); }).catch(next); } // Scenario: The user is a nutjob or spammed the back-button. // Action: Simply pass along the already established session. else { next(null, req.user); } } }).catch(next); }; /** * Create an authentication endpoint * * For more information on authentication in Passport.js, check out: * http://passportjs.org/guide/authenticate/ * * @param {Object} req * @param {Object} res */ passport.endpoint = function (req, res) { var strategies = sails.config.passport; var provider = req.param('provider'); var options = {}; // If a provider doesn't exist for this endpoint, send the user back to the // login page if (!_.has(strategies, provider)) { return res.redirect('/login'); } // Attach scope if it has been set in the config if (_.has(strategies[provider], 'scope')) { options.scope = strategies[provider].scope; } // Redirect the user to the provider for authentication. When complete, // the provider will redirect the user back to the application at // /auth/:provider/callback this.authenticate(provider, options)(req, res, req.next); }; /** * Create an authentication callback endpoint * * For more information on authentication in Passport.js, check out: * http://passportjs.org/guide/authenticate/ * * @param {Object} req * @param {Object} res * @param {Function} next */ passport.callback = function (req, res, next) { var provider = req.param('provider', 'local'); var action = req.param('action'); // Passport.js wasn't really built for local user registration, but it's nice // having it tied into everything else. if (provider === 'local' && action !== undefined) { if (action === 'register' && !req.user) { this.protocols.local.register(req, res, next); } else if (action === 'connect' && req.user) { this.protocols.local.connect(req, res, next); } else if (action === 'disconnect' && req.user) { this.protocols.local.disconnect(req, res, next); } else { next(new Error('Invalid action')); } } else { if (action === 'disconnect' && req.user) { this.disconnect(req, res, next); } else { // The provider will redirect the user to this URL after approval. Finish // the authentication process by attempting to obtain an access token. If // access was granted, the user will be logged in. Otherwise, authentication // has failed. this.authenticate(provider, next)(req, res, req.next); } } }; /** * Load all strategies defined in the Passport configuration * * For example, we could add this to our config to use the GitHub strategy * with permission to access a users email address (even if it's marked as * private) as well as permission to add and update a user's Gists: * github: { name: 'GitHub', protocol: 'oauth2', scope: [ 'user', 'gist' ] options: { clientID: 'CLIENT_ID', clientSecret: 'CLIENT_SECRET' } } * * For more information on the providers supported by Passport.js, check out: * http://passportjs.org/guide/providers/ * */ passport.loadStrategies = function () { var strategies = sails.config.passport; _.each(strategies, _.bind(function (strategy, key) { var options = { passReqToCallback: true }; var Strategy; if (key === 'local') { // Since we need to allow users to login using both usernames as well as // emails, we'll set the username field to something more generic. _.extend(options, { usernameField: 'identifier' }); // Only load the local strategy if it's enabled in the config if (strategies.local) { Strategy = strategies[key].strategy; passport.use(new Strategy(options, this.protocols.local.login)); } } else { var protocol = strategies[key].protocol; var callback = strategies[key].callback; if (!callback) { callback = path.join('auth', key, 'callback'); } Strategy = strategies[key].strategy; var baseUrl = ''; if (sails.config.appUrl && sails.config.appUrl !== null) { baseUrl = sails.config.appUrl; } else { sails.log.warn('Please add "appUrl" configuration value.'); baseUrl = sails.getBaseurl(); } switch (protocol) { case 'oauth': case 'oauth2': options.callbackURL = url.resolve(baseUrl, callback); break; case 'openid': options.returnURL = url.resolve(baseUrl, callback); options.realm = baseUrl; options.profile = true; break; } // Merge the default options with any options defined in the config. All // defaults can be overriden, but I don't see a reason why you'd want to // do that. _.extend(options, strategies[key].options); passport.use(key, new Strategy(options, this.protocols[protocol])); } }, passport)); }; /** * Disconnect a passport from a user * * @param {Object} req * @param {Object} res */ passport.disconnect = function (req, res, next) { var user = req.user; var provider = req.param('provider'); return sails.models.passport.findOne({ provider: provider, user: user.id }).then(function (passport) { return sails.models.passport.destroy({ id: passport.id }); }).then(function (error) { next(null, user); return user; }).catch(next); }; passport.serializeUser(function (user, next) { next(null, user.id); }); passport.deserializeUser(function (id, next) { return sails.services.authservice.findUser({ id: id }).then(function (user) { next(null, user || null); return user; }).catch(next); }); module.exports = passport; }