/** * Module dependencies. */ var util = require('util'), OAuth2Strategy = require('passport-oauth').OAuth2Strategy, axios = require('axios'), pkg = require('../package.json'), crypto = require('crypto'), querystring = require('querystring'); function encodeClientInfo(obj) { return Buffer.from(JSON.stringify(obj)).toString('base64'); } var clientInfoHeader = encodeClientInfo({ name: 'passport-auth0', version: pkg.version, env: { node: process.version } }); var Profile = require('./Profile'); var verifyWrapper = require('./verifyWrapper'); /** * `Strategy` constructor. * * * @param {Object} options * @param {Function} verify * @api public */ function Strategy(options, verify) { ['domain', 'clientID', 'clientSecret', 'callbackURL'].forEach(function (k) { if(!options[k]){ throw new Error('You must provide the ' + k + ' configuration value to use passport-auth0.'); } }); options.customHeaders = Object.assign( { 'Auth0-Client': clientInfoHeader }, typeof options.customHeaders === 'object' ? options.customHeaders : {} ); var defaultOptions = { expectedIssuer: 'https://' + options.domain + '/', authorizationURL: 'https://' + options.domain + '/authorize', tokenURL: 'https://' + options.domain + '/oauth/token', userInfoURL: 'https://' + options.domain + '/userinfo', apiUrl: 'https://' + options.domain + '/api' } this.options = Object.assign({}, defaultOptions, options); if (this.options.state === undefined) { this.options.state = true; } this._base = Object.getPrototypeOf(Strategy.prototype); this._base.constructor.call(this, this.options, verify); this.name = 'auth0'; } /** * Inherit from `OAuth2Strategy`. */ util.inherits(Strategy, OAuth2Strategy); Strategy.prototype.authenticate = function (req, options) { if (req.query && req.query.error) { return this.fail(req.query.error); } if (this.options.state) { if (!req.session) { throw new Error('Auth0Strategy requires you set state to false when no session is present') } if (req.query.code) { // If the code parameter is present, authenticate() is being called on the callback route. this._verify = verifyWrapper(this._verify, this.options, req.session.authParams); } else { // If the code parameter is not present, authenticate() is being called on the login route. req.session.authParams = {}; req.session.authParams.scope = options.scope; req.session.authParams.nonce = crypto.randomBytes(16).toString('hex'); this.authParams = req.session.authParams } } else if (options.scope && options.scope.includes('openid')) { throw new Error('Scope "openid" is not allowed without Auth0Strategy state true') } this._base.authenticate.call(this, req, options); }; Strategy.prototype.authorizationParams = function(options) { var options = options || {}; var params = Object.assign({}, options); /* You might wonder why we have delete statements here? The objective is to make it possible for consumers to use the API like this: passport.authenticate('auth0', { scope: 'openid email profile', 'my_custom_extra_param': true // <== An extra param }) To keep the validation of all expected options, we therefore take a copy and delete the "reserved" options. This leaves us with any extra params. When node.js version 6 is dropped at one point, this can be done in a single line of code 😇 const {connection, connection_scope, audience, prompt, login_hint, acr_value, nonce, max_age, ...theRest} = options; */ delete params.connection; delete params.connection_scope; delete params.audience; delete params.prompt; delete params.login_hint; delete params.acr_values; delete params.nonce; delete params.max_age; if (options.connection && typeof options.connection === 'string') { params.connection = options.connection; if (options.connection_scope && typeof options.connection_scope === 'string') { params.connection_scope = options.connection_scope; } } if (options.audience && typeof options.audience === 'string') { params.audience = options.audience; } if (options.prompt && typeof options.prompt === 'string') { params.prompt = options.prompt; } if (options.login_hint && typeof options.login_hint === 'string') { params.login_hint = options.login_hint; } if (options.acr_values && typeof options.acr_values === 'string') { params.acr_values = options.acr_values; } var strategyOptions = this.options; if (strategyOptions && typeof strategyOptions.maxAge === 'number') { params.max_age = strategyOptions.maxAge; } if (this.authParams && typeof this.authParams.nonce === 'string') { params.nonce = this.authParams.nonce; } return params; }; /** * @deprecated An Access Token is already included as part of the verify callback passed to Auth0Strategy. */ Strategy.prototype._getAccessToken = function(done){ var body = { 'client_id': this.options.clientID, 'client_secret': this.options.clientSecret, 'type': 'web_server', 'grant_type': 'client_credentials' }; axios({ method: 'POST', url: this.options.tokenURL, data: querystring.stringify(body), headers: { 'Auth0-Client': clientInfoHeader } }) .then((response) => done(null, response.data['access_token'], response.data['id_token'])) .catch((err) => done(err)); }; /** * Retrieve user profile from Auth0. * * This function constructs a normalized profile, with the following properties: * * - `provider` this is the strategy (google-oauth2, google, office365, google-apps) * - `id` this is the user_id of the auth0 profile * - `username` this is the nickname of the auth0 profile * - `displayName` * * @param {String} accessToken * @param {Function} done * @api protected */ Strategy.prototype.userProfile = function(accessToken, done) { this._oauth2.get(this.options.userInfoURL, accessToken, function (err, body, res) { if (err) { return done(new Error('failed to fetch user profile', err)); } try { var json = JSON.parse(body); var profile = new Profile(json, body); done(null, profile); } catch(e) { done(e); } }); }; /** * Expose `Strategy` directly from package. */ exports = module.exports = Strategy; /** * Export constructors. */ exports.Strategy = Strategy;