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 (221 loc) 9.96 kB
'use strict'; // core modules const path = require('path'); // npm modules const _ = require('lodash'); const cookieParserFactory = require('cookie-parser'); const loopback = require('loopback'); // eslint-disable-line import/no-unresolved const logger = require('./logger'); const setupCustomAuthScheme = require('./authentication'); const oniyiConfig = require('oniyi-config'); const utils = require('./utils'); const providerRegistry = require('./provider-registry'); // load strongloop's module for 3rd party authentication // used to configure models and initialize `passport` on the loopback application const PassportConfigurator = require('loopback-component-passport').PassportConfigurator; function componentLoader(app, spec) { // make sure we have a loopback app running in a server environment if (!(app && app.loopback && app.loopback.isServer)) { throw new Error('`app` argument must be an instance of `loopback` running in a server environment'); } // check for `spec` argument to be of plain object type if (!_.isPlainObject(spec)) { throw new TypeError('`spec` argument must be a plain object'); } // compile componentOptions // 1. pick supported properties from provided `spec` // 2. set default values where no other value was provided // uses `defaultsDeep` to support subsets of nested componentOptions e.g. `models.userIdentityModel` const componentOptions = _.defaultsDeep({}, _.pick(spec, [ 'appRootDir', 'enableSessionSupport', 'serverBaseUrl', 'frontendBaseUrl', 'contextRoot', 'models', ]), { appRootDir: path.join(process.cwd(), 'server'), enableSessionSupport: false, serverBaseUrl: 'http://localhost:3000', frontendBaseUrl: spec.serverBaseUrl || 'http://localhost:3000', // if not set via `spec.frontendBaseUrl`, try to get value from `spec.serverBaseUrl` otherwise overwrite with default value contextRoot: '/auth', models: { // default loopback `User`, `UserIdentity`, `UserCredential` and `AccessToken` userModel: 'User', userIdentityModel: 'UserIdentity', userCredentialModel: 'UserCredential', accessTokenModel: 'AccessToken', }, }); const appAuthDir = path.join(componentOptions.appRootDir, 'authentication'); // resolve models provided as `String` to their actual instance Object .keys(componentOptions.models) .forEach((model) => { if (_.isString(componentOptions.models[model])) { // registry.getModel will throw an error when no such model exists componentOptions.models[model] = app.registry.getModel(componentOptions.models[model]); } }); // Create an instance of PassportConfigurator with the app instance const passportConfigurator = new PassportConfigurator(app); // Load the provider configurations const providers = oniyiConfig({ basePath: appAuthDir, baseName: 'providers', }); function setupAuthProvider(name, options) { // load body parser middlewares for all `options.route` // this step is required even for those providers that are passed to `loopback-component-passport`, // since some of those might still require bodyParsing (e.g. when using built-in LDAP or local auth) Object.keys(options.route).forEach((routeName) => { const route = options.route[routeName]; // abort when method is `HEAD` or `GET`. Those don't support HTTP Request Body if (/(HEAD|GET)/i.test(route.method)) { return; } const routeBodyParser = utils.makeBodyParserMiddleware(routeName, route); app.middleware('auth:before', route.path, routeBodyParser); }); // setup auth middleware for link providers' subsequent calls. // this enables us to read accessToken information from additional sources on requests subsequent to // the link initiation (e.g. on OAuth 2.0 callback) if (options.provider.link) { const routePaths = _.uniq(Object.keys(options.route).map((routeName) => { return options.route[routeName].path; })); const routeMethods = _.uniq(Object.keys(options.route).map((routeName) => { return options.route[routeName].method; })); // @TODO: add token middleware to read accessToken from `state` param if (options.provider.useTokenCookieForCallback) { const tokenCookie = utils.tokenCookie(name, options.provider.path); // load cookie parser middleware to enable signed cookies // *Note*: "signedCookieSecret" must be set as app parameter app.middlewareFromConfig(cookieParserFactory, { enabled: true, phase: 'auth:after', params: [app.get('signedCookieSecret'), {}], methods: routeMethods, paths: routePaths, }); // set cookie with authenticated user's accessToken // this is needed in order to have a reference to the authenticated user when in callback handler app.middleware('auth:after', options.route.auth.path, (req, res, next) => { // abort middleware if the request http method is different from the configured auth http method if (req.method !== options.route.auth.method) { return next(); } const requestPath = `${req.baseUrl}${req.path}`.replace(/\/$/, ''); if (!requestPath.endsWith(options.route.auth.path.replace(/\/$/, ''))) { return next(); } // abort middleware if "code" parameter is providede in query // --> indicator for callback request from oauth provider // --> which happens when authPath === callbackPath if (req.query && req.query.code) { return next(); } // this is obviously an "authorize / link" request --> being authenticated is a strict requirement now if (!req.accessToken) { res.clearCookie(tokenCookie.name, { path: tokenCookie.options.path, }); return res.status(401).end('No accessToken found in request object'); } // user is authenticated, set signed cookie with current accessToken.id as value // we'll need that token when we handle the callback request res.cookie(tokenCookie.name, req.accessToken.id, tokenCookie.options); return next(); }); // make sure that the previously stored token (in a cookie) is loaded from request // before any next provider route handler is initialized app.middlewareFromConfig(loopback.token, { enabled: true, phase: 'auth:after', params: { model: componentOptions.models.accessTokenModel, currentUserLiteral: false, searchDefaultTokenKeys: false, enableDoublecheck: true, overwriteExistingToken: false, cookies: [tokenCookie.name], }, methods: routeMethods, paths: routePaths, }); // clear cookie on first access app.middleware('auth:after', routePaths, (req, res, next) => { if (req.signedCookies[tokenCookie.name]) { res.clearCookie(tokenCookie.name, { path: tokenCookie.options.path, }); } next(); }); } } // if authScheme is one of the loopback supported ones, let loopback-component-passport handle the setup // when there is no authScheme provided, we also pass on to loopback-component-passport. // They will assume OAuth 2.0 if (!options.provider.authScheme || ['ldap', 'local', 'oauth', 'oauth1', 'oauth 1.0', 'openid', 'openid connect', 'oauth 2.0', ].indexOf(options.provider.authScheme) > -1) { // configure provider with `loopback-component-passport` return passportConfigurator.configureProvider(name, utils.makeLegacyProviderOptions(options)); } return setupCustomAuthScheme(app, appAuthDir, componentOptions.models, name, options ); } // Initialize passport passportConfigurator.init(componentOptions.enableSessionSupport); // Set up related models passportConfigurator.setupModels(componentOptions.models); const dataSource = utils.initDataSource(app, componentOptions); // load component specific models, attach our datasource and attach the model to `app` instance const modelsCommonPath = path.join(__dirname, '..', 'models'); [ { name: 'authStrategyOptions', public: false, attachToApp: false }, { name: 'authSchemeOptions', public: false, attachToApp: false }, { name: 'authProvider', public: true, attachToApp: true }, ].forEach((modelInfo) => { /* eslint-disable global-require */ const definition = require(path.join(modelsCommonPath, `${modelInfo.name}.json`)); const customize = require(path.join(modelsCommonPath, `${modelInfo.name}.js`)); /* eslint-enable global-require */ const model = loopback.createModel(definition); customize(model); if (!!modelInfo.attachToApp) { // attach the model to our app // that makes it appear in the swagger spec and enables the rest endpoints app.model(model, { dataSource, public: !!modelInfo.public, }); } }); // Configure passport strategies for third party auth providers Object.keys(providers) .forEach((providerName) => { const options = utils.initProviderOptions(providerName, providers[providerName], componentOptions); providerRegistry.add(providerName, options); // abort registration if the provider is configured to be disabled if (!!options.provider.disabled) { logger.debug(`ignoring disabled auth provider ${providerName}`); return false; } return setupAuthProvider(providerName, options); }); // enable authentication app.enableAuth(); } module.exports = componentLoader;