UNPKG

loopback-component-auth

Version:

Extends loopback-component-passport to support custom auth schemes (i.e. other than the supported 'ldap', 'local', 'oauth', 'oauth1', 'oauth 1.0', 'openid', 'openid connect' and 'oauth 2.0')

259 lines (222 loc) 8.35 kB
/* eslint global-require: 0 */ 'use strict'; // core modules const path = require('path'); // npm modules const _ = require('lodash'); const passport = require('passport'); // local modules const logger = require('../logger'); const utils = require('../utils'); module.exports = function setupCustomAuthScheme(app, appAuthDir, models, name, options) { const passportModule = require(options.provider.module); const AuthStrategy = passportModule[options.provider.strategy]; const authSchemeModule = require(path.join(appAuthDir, options.provider.authScheme)); // restore "returnTo" url (if any), set by ensureLoggedIn and remove it from the session function successRedirect(req) { if (req && req.session && req.session.returnTo) { const returnTo = req.session.returnTo; /* eslint-disable no-param-reassign */ delete req.session.returnTo; /* eslint-enable no-param-reassign */ return returnTo; } return options.strategy.successRedirect; } const failureRedirect = options.strategy.failureRedirect; const makeLoginCallback = options.provider.makeLoginCallback || function makeLoginCallback(done) { return (err, user, identity, token) => { const authInfo = { identity, }; if (err) { return done(err, user, authInfo); } if (token) { authInfo.accessToken = token; } return done(err, user, authInfo); }; }; // call authScheme specific method to generate a verify function for passport const verifyFunction = authSchemeModule.makeVerifyFunction(options, models, makeLoginCallback); // register this provider in passport passport.use(name, new AuthStrategy( _.defaults({}, options.strategy, { authInfo: true, passReqToCallback: true, }), verifyFunction )); const passportAuthOptions = _.defaults({}, options.provider, options.strategy); // provide default handling of OAuth 2.0 flow function defaultAuthenticateMiddleware(req, res, next) { // unified handling of a successful auth attempt (regardless of enabled / disabled sessions) function handleAuthSuccess(user, info) { // this is a JSON enabled provider => return access_token and userId accordingly // finish response if (options.provider.json) { return res.status(200).json({ state: 'success', provider_name: name, userId: user.id, access_token: (info && info.accessToken) ? info.accessToken.id : undefined, }); } if (info && info.accessToken) { // we have a legacy client that needs information to be transmitted as cookies res.cookie('access_token', info.accessToken.id, { signed: !!req.signedCookies, // maxAge is in ms maxAge: 1000 * info.accessToken.ttl, domain: _.isString(options.provider.domain) ? options.provider.domain : null, }); res.cookie('userId', user.id.toString(), { signed: !!req.signedCookies, maxAge: 1000 * info.accessToken.ttl, domain: _.isString(options.provider.domain) ? options.provider.domain : null, }); } // finally redirect our client to the success page const redirectTo = utils.appendQuery(successRedirect(req), { state: 'success', provider_name: name, }); return res.redirect(redirectTo); } // The default callback passport.authenticate(name, passportAuthOptions, (err, user, info) => { if (err) { return next(err); } // handle unsuccessful logins (user object was not resolved truthy) if (!user) { if (options.provider.json) { return res.status(401).json({ state: 'failure', provider_name: name, error_code: 401, error_message: 'authentication failed', }); } const redirectTo = utils.appendQuery(failureRedirect, { state: 'failure', provider_name: name, error_code: 401, error_message: 'authentication failed', }); return res.redirect(redirectTo); } // if there is no session support, handle auth success right away if (!options.provider.session) { return handleAuthSuccess(user, info); } // session is enabled, call req.login() manually to establish the secured session return req.logIn(user, (loginErr) => { if (loginErr) { return next(loginErr); } return handleAuthSuccess(user, info); }); })(req, res, next); } const defaultMiddleware = { auth: defaultAuthenticateMiddleware, callback: defaultAuthenticateMiddleware, }; const defaultLinkMiddleware = { auth: passport.authorize(name, passportAuthOptions), callback: [ (req, res, next) => { passport.authorize(name, passportAuthOptions, (authorizeError, user, infoOrChallange, status) => { if (authorizeError) { return next(authorizeError); } if (user) { req.account = user; // eslint-disable-line no-param-reassign return next(); } let errorMessage = infoOrChallange; if (!_.isString(errorMessage)) { try { errorMessage = JSON.stringify(errorMessage); } catch (stringifyError) { logger.error('can not stringify errorMessage object', stringifyError); } } const authorizeFailedError = new Error(errorMessage); authorizeFailedError.status = status || 403; return next(authorizeFailedError); })(req, res, next); }, // passport.authorize doesn't handle json providers, thus we need to ship our own success handler (req, res) => { // @TODO: create spec of what is returned in json version // the resolved account is in req.account now if (options.provider.json) { return res .status(200) .json({ state: 'success', provider_name: name, account: req.account, }); } const redirectTo = utils.appendQuery(successRedirect(req), { state: 'success', provider_name: name, }); return res.redirect(redirectTo); }, (err, req, res, next) => { logger.error(err); if (options.provider.json) { return res.status(err.status || 403).json({ state: 'failure', provider_name: name, error_code: err.status || 403, error_message: err.message, }); } // @TODO: req.flash does not exist since Expressjs 3.x if (options.strategy.failureFlash) { if (typeof req.flash !== 'function') { return next(new TypeError('req.flash is not a function')); } let flash = options.strategy.failureFlash; if (typeof flash === 'string') { flash = { type: 'error', message: flash, }; } const type = flash.type || 'error'; const msg = flash.message || err.message; if (typeof msg === 'string') { req.flash(type, msg); } } const redirectTo = utils.appendQuery(failureRedirect, { state: 'failure', provider_name: name, error_code: err.code || 403, error_message: err.message, }); return res.redirect(redirectTo); }, ], }; // register handlers for each route configured in provider Object.keys(options.route).forEach((routeName) => { const route = options.route[routeName]; const middlewareCatalog = options.provider.link ? defaultLinkMiddleware : defaultMiddleware; const middleware = route.middleware || middlewareCatalog[routeName]; // check for valid middleware if (!(typeof middleware === 'function' || Array.isArray(middleware))) { throw new TypeError(`can not resolve "${routeName}" middleware for provider ${name}`); } app[route.method.toLowerCase()](route.path, middleware); logger.info(`registered handler for "${routeName}" route with path ${route.path}`); }); };