passport.authenticate('azuread-openidconnect', { failureRedirect: '/' }), function(req, res) { log.info('Login was called in the Sample'); res.redirect('/');
Strategy.prototype.authenticate = functionauthenticateStrategy(req, options) { /* * We should be careful using 'this'. Avoid the usage like `this.xxx = ...` * unless you know what you are doing. * * In the passport source code * (https://github.com/jaredhanson/passport/blob/master/lib/middleware/authenticate.js) * when it attempts to call the `oidcstrategy.authenticate` function, passport * creates an instance inherting oidcstrategy and then calls `instance.authenticate`. * Therefore, when we come here, `this` is the instance, its prototype is the * actual oidcstrategy, i.e. the `Strategy`. This means: * (1) `this._options = `, `this._verify = `, etc only adds new fields to the * instance, it doesn't change the values in oidcstrategy, i.e. `Strategy`. * (2) `this._options`, `this._verify`, etc returns the field in the instance, * and if there is none, returns the field in oidcstrategy, i.e. `strategy`. * (3) each time we call `authenticate`, we will get a brand new instance * * If you want to change the values in `Strategy`, use * const oidcstrategy = Object.getPrototypeOf(self); * to get the strategy first. * * Note: Simply do `const self = Object.getPrototypeOf(this)` and use `self` * won't work, since the `this` instance has a couple of functions like * success/fail/error... which `authenticate` will call. The following is the * structure of `this`: * * this * | -- success: function(user, info) * | -- fail: function(challenge, status) * | -- redirect: function(url, status) * | -- pass: function() * | -- __proto__: Strategy * | -- _verify * | -- _options * | -- ... * | -- __proto__: * | -- authenticate: function(req, options) * | -- ... */ const self = this;
var resource = options && options.resourceURL; var customState = options && options.customState; var tenantIdOrName = options && options.tenantIdOrName; var login_hint = options && options.login_hint; var domain_hint = options && options.domain_hint; var prompt = options && options.prompt; var extraAuthReqQueryParams = options && options.extraAuthReqQueryParams; var extraTokenReqQueryParams = options && options.extraTokenReqQueryParams; var response = options && options.response || req.res;
// 'params': items we get from the request or metadata, such as id_token, code, policy, metadata, cacheKey, etc var params = { 'tenantIdOrName': tenantIdOrName, 'extraAuthReqQueryParams': extraAuthReqQueryParams, 'extraTokenReqQueryParams': extraTokenReqQueryParams }; // 'oauthConfig': items needed for oauth flow (like redirection, code redemption), such as token_endpoint, userinfo_endpoint, etc var oauthConfig = { 'resource': resource, 'customState': customState, 'domain_hint': domain_hint, 'login_hint': login_hint, 'prompt': prompt, 'response': response }; // 'optionsToValidate': items we need to validate id_token against, such as issuer, audience, etc var optionsToValidate = {};
async.waterfall( [ /***************************************************************************** * Step 1. Collect information from the req and save the info into params ****************************************************************************/ (next) => { return self.collectInfoFromReq(params, req, next, response); },
/***************************************************************************** * Step 2. Load metadata, use the information from 'params' and 'self._options' * to configure 'oauthConfig' and 'optionsToValidate' ****************************************************************************/ (next) => { return self.setOptions(params, oauthConfig, optionsToValidate, next); },
/***************************************************************************** * Step 3. Handle the flows *---------------------------------------------------------------------------- * (1) implicit flow (response_type = 'id_token') * This case we get a 'id_token' * (2) hybrid flow (response_type = 'id_token code') * This case we get both 'id_token' and 'code' * (3) authorization code flow (response_type = 'code') * This case we get a 'code', we will use it to get 'access_token' and 'id_token' * (4) for any other request, we will ask for authorization and initialize * the authorization process ****************************************************************************/ (next) => { if (params.err) { // handle the error return self._errorResponseHandler(params.err, params.err_description, next); } elseif (!params.id_token && !params.code) { // ask for authorization, initialize the authorization process return self._flowInitializationHandler(oauthConfig, req, next); } elseif (params.id_token && params.code) { // handle hybrid flow return self._hybridFlowHandler(params, oauthConfig, optionsToValidate, req, next); } elseif (params.id_token) { // handle implicit flow return self._implicitFlowHandler(params, optionsToValidate, req, next); } else { // handle authorization code flow return self._authCodeFlowHandler(params, oauthConfig, optionsToValidate, req, next); } } ],
(waterfallError) => { // this code gets called after the three steps above are done if (waterfallError) { return self.failWithLog(`${aadutils.getErrorMessage(waterfallError)}`); } returntrue; }); };
Strategy.prototype._flowInitializationHandler = functionflowInitializationHandler(oauthConfig, req, next) { // The request being authenticated is initiating OpenID Connect // authentication. Prior to redirecting to the provider, configuration will // be loaded. The configuration is typically either pre-configured or // discovered dynamically. When using dynamic discovery, a user supplies // their identifer as input.
// the things we will put into 'params': // err, err_description, id_token, code, policy, state, nonce, cachekey, metadata
// ------------------------------------------------------------------------- // we shouldn't get any access_token or refresh_token from the request // ------------------------------------------------------------------------- if ((req.query && (req.query.access_token || req.query.refresh_token)) || (req.body && (req.body.access_token || req.body.refresh_token))) returnnext(newError('In collectInfoFromReq: neither access token nor refresh token is expected in the incoming request'));
// ------------------------------------------------------------------------- // we might get err, id_token, code, state from the request // ------------------------------------------------------------------------- var source = null;
if (source) { params.err = source.error; params.err_description = source.error_description; params.id_token = source.id_token; params.code = source.code; params.state = source.state; if (source.state && source.state.length >= 38) { // the random generated state always has 32 characters. This state is longer than 32 // so it must be a custom state. We added 'CUSTOM' prefix and a random 32 byte long // string in front of the original custom state, now we change it back. if (!source.state.startsWith('CUSTOM')) returnnext(newError(`In collectInfoFromReq: invalid custom state ${state}`));
source.state = source.state.substring(38); } }
// ------------------------------------------------------------------------- // If we received code, id_token or err, we must have received state, now we // find the state/nonce/policy tuple from session. // If we received none of them, find policy in query // ------------------------------------------------------------------------- if (params.id_token || params.code || params.err) { if (!params.state) returnnext(newError('In collectInfoFromReq: missing state in the request'));
// user provided tenantIdOrName will be ignored for redirectUrl, since we saved the one we used in session if (params.tenantIdOrName) { if (self._options.loggingNoPII) log.info('user provided tenantIdOrName is ignored for redirectUrl, we will use the one stored in session'); else log.info(`user provided tenantIdOrName '${params.tenantIdOrName}' is ignored for redirectUrl, we will use the one stored in session`); } params.tenantIdOrName = tuple['tenantIdOrName']; } else { params.policy = req.query.p ? req.query.p.toLowerCase() : null; }
// if we are not using the common endpoint, but we have tenantIdOrName, just ignore it if (!self._options.isCommonEndpoint && params.tenantIdOrName) { if (self._options.loggingNoPII) log.info('identityMetadata is tenant-specific, so we ignore the tenantIdOrName provided'); else log.info(`identityMetadata is tenant-specific, so we ignore the tenantIdOrName '${params.tenantIdOrName}'`); params.tenantIdOrName = null; }
// if we are using the common endpoint and we want to validate issuer, then user has to // provide issuer in config, or provide tenant id or name using tenantIdOrName option in // passport.authenticate. Otherwise we won't know the issuer. if (self._options.isCommonEndpoint && self._options.validateIssuer && (!self._options.issuer && !params.tenantIdOrName)) returnnext(newError('In collectInfoFromReq: issuer or tenantIdOrName must be provided in order to validate issuer on common endpoint'));
// for B2C, we must have policy if (self._options.isB2C && !params.policy) returnnext(newError('In collectInfoFromReq: policy is missing')); // for B2C, if we are using common endpoint, we must have tenantIdOrName provided if (self._options.isB2C && self._options.isCommonEndpoint && !params.tenantIdOrName) returnnext(newError('In collectInfoFromReq: we are using common endpoint for B2C but tenantIdOrName is not provided'));
// ------------------------------------------------------------------------- // calculate metadataUrl, create a cachekey and an Metadata object instance // we will fetch the metadata, save it into the object using the cachekey // ------------------------------------------------------------------------- var metadataUrl = self._options.identityMetadata;
// if we are using common endpoint and we are given the tenantIdOrName, let's replace it if (self._options.isCommonEndpoint && params.tenantIdOrName) { metadataUrl = metadataUrl.replace('/common/', `/${params.tenantIdOrName}/`); if (self._options.loggingNoPII) log.info(`we are replacing 'common' with the tenantIdOrName provided`); else log.info(`we are replacing 'common' with the tenantIdOrName ${params.tenantIdOrName}`); }
// add policy for B2C if (self._options.isB2C) metadataUrl = metadataUrl.concat(`&p=${params.policy}`);
// we use the metadataUrl as the cachekey params.cachekey = metadataUrl; params.metadata = newMetadata(metadataUrl, 'oidc', self._options);
if (!self._options.loggingNoPII) { log.info(`metadataUrl is: ${metadataUrl}`); log.info(`received the following items in params: ${JSON.stringify(params)}`); }