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')
250 lines (209 loc) • 8.05 kB
JavaScript
const url = require('url');
const bodyParser = require('body-parser');
const _ = require('lodash');
const logger = require('../logger');
const initDataSource = require('./datasource');
function httpMethod(method) {
if (!_.isString(method)) {
return 'GET';
}
return /^(GET|POST)$/i.test(method) ? method.toUpperCase() : 'GET';
}
function routePath(contextRoot, providerName, routeName) {
return `${contextRoot.replace(/\/$/, '')}/${providerName}/${routeName}`.toLowerCase();
}
function tokenCookie(name, path) {
const cookieName = `provider_${name}_token`;
const cookie = {
name: cookieName,
options: {
signed: true,
maxAge: 300000, // 300 seconds / 5 minutes
httpOnly: true,
path: path.replace(/\/?/, ''),
},
};
return cookie;
}
function makeBodyParserMiddleware(name, route) {
if (!(route.bodyParser && _.isString(route.bodyParser))) {
throw new Error(`"bodyParser" is a required option for route "${name}"
and must be of type "String" when using "method" other than "HEAD"|"GET"`);
}
if (!bodyParser[route.bodyParser]) {
throw new Error(`"${route.bodyParser}" is not a supported bodyParser`);
}
const bodyParserFactory = bodyParser[route.bodyParser];
const bodyParserOptions = route.bodyParserOptions || {};
return bodyParserFactory(bodyParserOptions);
}
function appendQuery(urlString, query) {
return url.format(
_.merge(url.parse(urlString), {
query,
}));
}
function makeSuccessRedirectUrl(params) {
const successRedirect = params.successRedirect || (params.link ?
'/account/authorize/result' :
'/account'
);
return url.resolve(params.baseUrl || '/', successRedirect);
}
function makeFailureRedirectUrl(params) {
const failureRedirect = params.failureRedirect || (params.link ?
'/account/authorize/result' :
'/account/login'
);
return url.resolve(params.baseUrl || '/', failureRedirect);
}
function appBaseUrl(app, options, callback) {
/**
* when useing `loopbackApp.listen()`, subscribes to the `http` server object's `listening`
* object and compiles `host`, `port` and `url` app properties from the actual tcp socket that the
* `http` server object is using. Unfortunately, the app is not listening when this component is initialized.
* With that said, we don't have the tcp socket active yet and can't extract actual information.
* An alternative could be to force setting `host`, `port` and `scheme` in the global application config.
* Also, emitting another event on `app` from the `listen` callback to fetch it here and then pick `app.get('url')`
* could be an option.
*/
// app.get('host');
// app.get('port');
// app.set('url', 'http://' + host + ':' + self.get('port') + '/');
app.once('started', () => {
callback(null, app.get('url'));
});
return callback(new Error('fetching the application\'s base url is not implemented yet'));
}
function initProviderOptions(name, options, componentOptions) {
// read all top-level properties from `options` except for those containing specially nested options
const providerOptions = _.defaults({},
_.omit(options, ['routeOptions', 'schemeOptions', 'strategyOptions']), {
strategy: 'Strategy',
link: false,
json: false,
session: false, // sessions are disabled if not set otherwise in providerOptions
useTokenCookieForCallback: false,
});
const routeOptions = _.defaultsDeep({}, options.routeOptions || {}, {
auth: {}, // make sure we always have an `auth` route
});
const schemeOptions = _.defaultsDeep({}, options.schemeOptions || {}, {});
const strategyOptions = _.defaultsDeep({}, options.strategyOptions || {}, {});
if (providerOptions.session && !componentOptions.enableSessionSupport) {
logger.warn(`can not enable session support for auth-provider "${name}".
Sessions are disabled globally.
Set component option "enableSessions" to true if you need session support`);
providerOptions.session = false;
}
providerOptions.authScheme = _.isString(providerOptions.authScheme) ? providerOptions.authScheme.toLowerCase() : null;
providerOptions.link = !!providerOptions.link;
providerOptions.path = `${componentOptions.contextRoot.replace(/\/$/, '')}/${name}`.toLowerCase();
// compute method and bodyParser for all provided route names
Object.keys(routeOptions).forEach((routeName) => {
let route = _.assign({}, routeOptions[routeName]);
const defaults = {
method: httpMethod(route.method),
// compute path from providerName, routeName and resolve from contextRoot
path: routePath(componentOptions.contextRoot, name, routeName),
};
// only set bodyParser default options when method allows request body
if (!/^(HEAD|GET)$/i.test(defaults.method)) {
route = _.merge({
bodyParser: 'json',
bodyParserOptions: {
extended: true,
},
}, route);
}
routeOptions[routeName] = _.defaults(defaults, route);
});
// when provider has `callback` route, compute callbackURL from previously resolved path
// this time resolved from a serverBaeUrl
if (routeOptions.callback && routeOptions.callback.path) {
strategyOptions.callbackURL = url.resolve(componentOptions.serverBaseUrl, routeOptions.callback.path);
}
// json enabled providers expect to receive results as response body and thus don't need redirect urls
if (!providerOptions.json) {
const successRedirect = makeSuccessRedirectUrl({
successRedirect: strategyOptions.successRedirect,
link: providerOptions.link,
baseUrl: componentOptions.frontendBaseUrl,
});
const failureRedirect = makeFailureRedirectUrl({
failureRedirect: strategyOptions.failureRedirect,
link: providerOptions.link,
baseUrl: componentOptions.frontendBaseUrl,
});
_.assign(strategyOptions, {
successRedirect,
failureRedirect,
});
}
return {
name,
provider: providerOptions,
route: routeOptions,
scheme: schemeOptions,
strategy: strategyOptions,
};
}
function makeLegacyProviderOptions(options) {
logger.warn(
`using legacy provider options is not fully supported yet.
This typically happens when your provider is configured to use
an authScheme handled by 'loopback-component-passport'`
);
// @TODO: compile authPath, callbackPath and all other properties requrired by `loopback-component-passport`
// provider: name
// json
// domain
// authPath: route.auth.path
// authOptions: from strategy? passed to passport.authenticate
// customCallback (route.auth.middleware for ldap scheme and also route.callback.middleware for link providers)
// successRedirect: strategy.successRedirect
// failureRedirect: strategy.failureRedirect
// authScheme: provider.authScheme
// loginCallback: scheme.makeLoginCallback ?
// passReqToCallback: strategy
// OAuth Scheme
// scope: strategy.scope
// callbackURL: strategy.callbackURL
// callbackPath: route.callback.path
// callbackHTTPMethod: route.callback.method .toLowerCase()
//
// LDAP Scheme:
// usernameField: strategy.usernameField
// passwordField: strategy.passwordField
// LdapAttributeForUsername
// LdapAttributeForMail
// LdapAttributeForLogin
// session: strategy / provider?
// authInfo: strategy / provider?
// passReqToCallback: strategy
//
// Local Scheme
// successReturnToOrRedirect
// usernameField: strategy.usernameField
// passwordField: strategy.passwordField
// session: strategy / provider?
// authInfo: strategy / provider?
// passReqToCallback: strategy
return _.assign({ provider: options.name },
options.provider,
options.strategy,
options.scheme
);
}
module.exports = {
tokenCookie,
makeBodyParserMiddleware,
appendQuery,
makeSuccessRedirectUrl,
makeFailureRedirectUrl,
initProviderOptions,
makeLegacyProviderOptions,
appBaseUrl,
initDataSource,
};
;