function Strategy(options, verifyFn) {
passport.Strategy.call(this);
this.name = 'oauth-bearer'; // Me, a name I call myself.
if (!options)
throw new Error('In BearerStrategy constructor: options is required');
if (!verifyFn || typeof verifyFn !== 'function')
throw new Error('In BearerStrategy constructor: verifyFn is required and it must be a function');
this._verify = verifyFn;
this._options = options;
//---------------------------------------------------------------------------
// Set up the default values
//---------------------------------------------------------------------------
// clock skew. Must be a postive integer
if (options.clockSkew && (typeof options.clockSkew !== 'number' || options.clockSkew <= 0 || options.clockSkew % 1 !== 0))
throw new Error('clockSkew must be a positive integer');
if (!options.clockSkew)
options.clockSkew = CONSTANTS.CLOCK_SKEW;
// default value of passReqToCallback is false
if (options.passReqToCallback !== true)
options.passReqToCallback = false;
// default value of validateIssuer is true
if (options.validateIssuer !== false)
options.validateIssuer = true;
// default value of allowMultiAudiencesInToken is false
if (options.allowMultiAudiencesInToken !== true)
options.allowMultiAudiencesInToken = false;
// if options.audience is a string or an array of string, then we use it;
// otherwise we use the clientID
if (options.audience && typeof options.audience === 'string')
options.audience = [options.audience];
else if (!options.audience || !Array.isArray(options.audience) || options.length === 0)
options.audience = [options.clientID, 'spn:' + options.clientID];
// default value of isB2C is false
if (options.isB2C !== true)
options.isB2C = false;
// turn issuer into an array
if (options.issuer === '')
options.issuer = null;
if (options.issuer && Array.isArray(options.issuer) && options.issuer.length === 0)
options.issuer = null;
if (options.issuer && !Array.isArray(options.issuer))
options.issuer = [options.issuer];
//---------------------------------------------------------------------------
// validate the things in options
//---------------------------------------------------------------------------
// clientID should not be empty
if (!options.clientID || options.clientID === '')
throw new Error('In BearerStrategy constructor: clientID cannot be empty');
// identityMetadata must be https url
if (!options.identityMetadata || !UrlValidator.isHttpsUri(options.identityMetadata))
throw new Error('In BearerStrategy constructor: identityMetadata must be provided and must be a https url');
//---------------------------------------------------------------------------
// treatment of common endpoint and issuer
//---------------------------------------------------------------------------
// check if we are using the common endpoint
options._isCommonEndpoint = (options.identityMetadata.indexOf('/common/') != -1);
// issuer validation for common endpoint is not supported
if (options._isCommonEndpoint && options.validateIssuer && !options.issuer)
throw new Error(`In BearerStrategy constructor: you are using common endpoint, please either set 'validateIssuer' to false,
or provide 'issuer' value in options`);
// give a warning if user is not validating issuer
if (!options.validateIssuer)
log.warn(`Production environments should always validate the issuer.`);
//---------------------------------------------------------------------------
// B2C.
// (1) policy must be provided and must have the valid prefix
// (2) common endpoint is not supported
//---------------------------------------------------------------------------
// for B2C,
if (options.isB2C) {
if (!options.policyName || !options.policyName.toLowerCase().startsWith(B2C_PREFIX))
throw new Error('In BearerStrategy constructor: invalid policy for B2C'); ...n/a
function Strategy() {
}n/a
function Strategy(options, verify) {
passport.Strategy.call(this);
/*
* Caution when you want to change these values in the member functions of
* Strategy, don't use `this`, since `this` points to a subclass of `Strategy`.
* To get `Strategy`, use Object.getPrototypeOf(this).
*
* More comments at the beginning of `Strategy.prototype.authenticate`.
*/
this._options = options;
this.name = 'azuread-openidconnect';
// stuff related to the verify function
this._verify = verify;
this._passReqToCallback = !!options.passReqToCallback;
if (options.useCookieInsteadOfSession === true)
this._useCookieInsteadOfSession = true;
else
this._useCookieInsteadOfSession = false;
if (!this._useCookieInsteadOfSession) {
// For each microsoft login page tab, we generate a {state, nonce, policy, timeStamp} tuple,
// normally user won't keep opening microsoft login page in new tabs without putting their
// password for more than 10 tabs, so we only keep the most recent 10 tuples in session.
// The lifetime of each tuple is 60 minutes or user specified.
this._sessionContentHandler = new SessionContentHandler(options.nonceMaxAmount || NONCE_MAX_AMOUNT, options.nonceLifetime ||
NONCE_LIFE_TIME);
} else {
this._cookieContentHandler = new CookieContentHandler(options.nonceMaxAmount || NONCE_MAX_AMOUNT, options.nonceLifetime || NONCE_LIFE_TIME
, options.cookieEncryptionKeys);
}
/* When a user is authenticated for the first time, passport adds a new field
* to req.session called 'passport', and puts a 'user' property inside (or your
* choice of field name and property name if you change passport._key and
* passport._userProperty values). req.session['passport']['user'] is usually
* user_id (or something similar) of the authenticated user to reduce the size
* of session. When the user logs out, req.session['passport']['user'] will be
* destroyed. Any request between login (when authenticated for the first time)
* and logout will have the 'user_id' in req.session['passport']['user'], so
* passport can fetch it, find the user object in database and put the user
* object into a new field: req.user. Then the subsequent middlewares and the
* app can use the user object. This is how passport keeps user authenticated.
*
* For state validation, we also take advantage of req.session. we create a new
* field: req.session[sessionKey], where the sessionKey is our choice (in fact,
* this._key, see below). When we send a request with state, we put state into
* req.session[sessionKey].state; when we expect a request with state in query
* or body, we compare the state in query/body with the one stored in
* req.session[sessionKey].state, and then destroy req.session[sessionKey].state.
* User can provide a state by using `authenticate(Strategy, {state: 'xxx'})`, or
* one will be generated automatically. This is essentially how passport-oauth2
* library does the state validation, and we use the same way in our library.
*
* request structure will look like the following. In real request some fields
* might not be there depending on the purpose of the request.
*
* request ---|--- sessionID
* |--- session --- |--- ...
* | |--- 'passport' ---| --- 'user': 'user_id etc'
* | |--- sessionKey---| --- state: 'xxx'
* |--- ...
* |--- 'user': full user info
*/
this._key = options.sessionKey || ('OIDC: ' + options.redirectUrl);
if (!options.identityMetadata) {
// default value should be https://login.microsoftonline.com/common/.well-known/openid-configuration
log.error('OIDCStrategy requires a metadata location that contains cert data for RSA and ECDSA callback.');
throw new TypeError(`OIDCStrategy requires a metadata location that contains cert data for RSA and ECDSA callback.`);
}
// if loggi ...n/a
function Strategy(options, verifyFn) {
passport.Strategy.call(this);
this.name = 'oauth-bearer'; // Me, a name I call myself.
if (!options)
throw new Error('In BearerStrategy constructor: options is required');
if (!verifyFn || typeof verifyFn !== 'function')
throw new Error('In BearerStrategy constructor: verifyFn is required and it must be a function');
this._verify = verifyFn;
this._options = options;
//---------------------------------------------------------------------------
// Set up the default values
//---------------------------------------------------------------------------
// clock skew. Must be a postive integer
if (options.clockSkew && (typeof options.clockSkew !== 'number' || options.clockSkew <= 0 || options.clockSkew % 1 !== 0))
throw new Error('clockSkew must be a positive integer');
if (!options.clockSkew)
options.clockSkew = CONSTANTS.CLOCK_SKEW;
// default value of passReqToCallback is false
if (options.passReqToCallback !== true)
options.passReqToCallback = false;
// default value of validateIssuer is true
if (options.validateIssuer !== false)
options.validateIssuer = true;
// default value of allowMultiAudiencesInToken is false
if (options.allowMultiAudiencesInToken !== true)
options.allowMultiAudiencesInToken = false;
// if options.audience is a string or an array of string, then we use it;
// otherwise we use the clientID
if (options.audience && typeof options.audience === 'string')
options.audience = [options.audience];
else if (!options.audience || !Array.isArray(options.audience) || options.length === 0)
options.audience = [options.clientID, 'spn:' + options.clientID];
// default value of isB2C is false
if (options.isB2C !== true)
options.isB2C = false;
// turn issuer into an array
if (options.issuer === '')
options.issuer = null;
if (options.issuer && Array.isArray(options.issuer) && options.issuer.length === 0)
options.issuer = null;
if (options.issuer && !Array.isArray(options.issuer))
options.issuer = [options.issuer];
//---------------------------------------------------------------------------
// validate the things in options
//---------------------------------------------------------------------------
// clientID should not be empty
if (!options.clientID || options.clientID === '')
throw new Error('In BearerStrategy constructor: clientID cannot be empty');
// identityMetadata must be https url
if (!options.identityMetadata || !UrlValidator.isHttpsUri(options.identityMetadata))
throw new Error('In BearerStrategy constructor: identityMetadata must be provided and must be a https url');
//---------------------------------------------------------------------------
// treatment of common endpoint and issuer
//---------------------------------------------------------------------------
// check if we are using the common endpoint
options._isCommonEndpoint = (options.identityMetadata.indexOf('/common/') != -1);
// issuer validation for common endpoint is not supported
if (options._isCommonEndpoint && options.validateIssuer && !options.issuer)
throw new Error(`In BearerStrategy constructor: you are using common endpoint, please either set 'validateIssuer' to false,
or provide 'issuer' value in options`);
// give a warning if user is not validating issuer
if (!options.validateIssuer)
log.warn(`Production environments should always validate the issuer.`);
//---------------------------------------------------------------------------
// B2C.
// (1) policy must be provided and must have the valid prefix
// (2) common endpoint is not supported
//---------------------------------------------------------------------------
// for B2C,
if (options.isB2C) {
if (!options.policyName || !options.policyName.toLowerCase().startsWith(B2C_PREFIX))
throw new Error('In BearerStrategy constructor: invalid policy for B2C'); ...n/a
function Strategy() {
}n/a
function authenticateStrategy(req) {
const self = this;
var params = {};
var optionsToValidate = {};
/* Some introduction to async.waterfall (from the following link):
* http://stackoverflow.com/questions/28908180/what-is-a-simple-implementation-of-async-waterfall
*
* Runs the tasks array of functions in series, each passing their results
* to the next in the array. However, if any of the tasks pass an error to
* their own callback, the next function is not executed, and the main callback
* is immediately called with the error.
*
* Example:
*
* async.waterfall([
* function(callback) {
* callback(null, 'one', 'two');
* },
* function(arg1, arg2, callback) {
* // arg1 now equals 'one' and arg2 now equals 'two'
* callback(null, 'three');
* },
* function(arg1, callback) {
* // arg1 now equals 'three'
* callback(null, 'done');
* }
* ], function (err, result) {
* // result now equals 'done'
* });
*/
async.waterfall([
// compute metadataUrl
(next) => {
params.metadataURL = self._options.identityMetadata
.concat(`?${aadutils.getLibraryProductParameterName()}=${aadutils.getLibraryProduct()}`)
.concat(`&${aadutils.getLibraryVersionParameterName()}=${aadutils.getLibraryVersion()}`); ;
if (self._options.isB2C)
params.metadataURL = params.metadataURL.concat(`&p=${self._options.policyName}`)
params.cacheKey = params.metadataURL;
log.info(`In Strategy.prototype.authenticate: ${JSON.stringify(params)}`);
return next(null, params);
},
// load metatadata
(params, next) => {
return self.loadMetadata(params, next);
},
// configure using metadata
(metadata, next) => {
params.metadata = metadata;
log.info(`In Strategy.prototype.authenticate: received metadata: ${JSON.stringify(metadata)}`);
// set up issuer
if (self._options.validateIssuer && !self._options.issuer)
optionsToValidate.issuer = metadata.oidc.issuer;
else
optionsToValidate.issuer = self._options.issuer;
// set up algorithm
optionsToValidate.algorithms = metadata.oidc.algorithms;
// set up audience, validateIssuer, allowMultiAudiencesInToken
optionsToValidate.audience = self._options.audience;
optionsToValidate.validateIssuer = self._options.validateIssuer;
optionsToValidate.allowMultiAudiencesInToken = self._options.allowMultiAudiencesInToken;
optionsToValidate.ignoreExpiration = self._options.ignoreExpiration;
// clock skew
optionsToValidate.clockSkew = self._options.clockSkew;
log.info(`In Strategy.prototype.authenticate: we will validate the following options: ${optionsToValidate}`);
return next();
},
// extract the access token from the request, after getting the token, it
// will call `jwtVerify` to verify the token. If token is verified, `jwtVerify`
// will provide the token payload to self._verify function. self._verify is
// provided by the developer, it's up to the developer to decide if the token
// payload is considered authenticated. If authenticated, self._verify will
// provide `user` object (developer's decision of its content) to `verified`
// function here, and the `verified` function does the final work of stuffing
// the `user` obejct into req.user, so the following middleware can use it.
// This is basically how bearerStrategy works.
(next) => {
var token;
// token could be in header or body. query is not supported.
if (req.query && req.query.access_token)
return self.failWithLog('In Strategy.prototype.authenticate: access_token should be passed in request header or body. query
is unsupported');
if (req.headers && req.headers.authorization) {
var auth_components = req.headers.authorization.split(' ');
if (auth_ ......
To complete the sample, provide a route that corresponds to the path
configuration parameter that is sent to the strategy:
```javascript
app.get('/login',
passport.authenticate('azuread-openidconnect', { failureRedirect: '/
x27; }),
function(req, res) {
log.info('Login was called in the Sample');
res.redirect('/');
});
// POST /auth/openid/return
// Use passport.authenticate() as route middleware to authenticate the
...failWithLog = function (message) {
log.info(`authentication failed due to: ${message}`);
return this.fail(message);
}n/a
function jwtVerifyFunc(req, token, metadata, optionsToValidate, done) {
const self = this;
const decoded = jws.decode(token);
let PEMkey = null;
if (decoded == null) {
return done(null, false, 'In Strategy.prototype.jwtVerify: Invalid JWT token.');
}
log.info('In Strategy.prototype.jwtVerify: token decoded: ', decoded);
// When we generate the PEMkey, there are two different types of token signatures
// we have to validate here. One provides x5t and the other a kid. We need to call
// the right one.
if (decoded.header.x5t) {
PEMkey = metadata.generateOidcPEM(decoded.header.x5t);
} else if (decoded.header.kid) {
PEMkey = metadata.generateOidcPEM(decoded.header.kid);
} else {
return self.failWithLog('In Strategy.prototype.jwtVerify: We did not receive a token we know how to validate');
}
log.info('PEMkey generated: ' + PEMkey);
jwt.verify(token, PEMkey, optionsToValidate, (err, verifiedToken) => {
if (err) {
if (err.message)
return self.failWithLog(err.message);
else
return self.failWithLog('In Strategy.prototype.jwtVerify: cannot verify id token');
}
log.info('In Strategy.prototype.jwtVerify: VerifiedToken: ', verifiedToken);
if (self._options.passReqToCallback) {
log.info('In Strategy.prototype.jwtVerify: We did pass Req back to Callback');
return self._verify(req, verifiedToken, done);
} else {
log.info('In Strategy.prototype.jwtVerify: We did not pass Req back to Callback');
return self._verify(verifiedToken, done);
}
});
}n/a
loadMetadata = function (params, next) {
const self = this;
var metadata = new Metadata(params.metadataURL, 'oidc', self._options);
// fetch metadata
return memoryCache.wrap(params.cacheKey, (cacheCallback) => {
metadata.fetch((fetchMetadataError) => {
if (fetchMetadataError) {
return self.failWithLog('In loadMetadata: Unable to fetch metadata');
}
return cacheCallback(null, metadata);
});
}, { ttl }, next);
}n/a
function Strategy() {
}n/a
function Strategy() {
}n/a
authenticate = function (req, options) {
throw new Error('Strategy#authenticate must be overridden by subclass');
}...
To complete the sample, provide a route that corresponds to the path
configuration parameter that is sent to the strategy:
```javascript
app.get('/login',
passport.authenticate('azuread-openidconnect', { failureRedirect: '/
x27; }),
function(req, res) {
log.info('Login was called in the Sample');
res.redirect('/');
});
// POST /auth/openid/return
// Use passport.authenticate() as route middleware to authenticate the
...function Strategy(options, verify) {
passport.Strategy.call(this);
/*
* Caution when you want to change these values in the member functions of
* Strategy, don't use `this`, since `this` points to a subclass of `Strategy`.
* To get `Strategy`, use Object.getPrototypeOf(this).
*
* More comments at the beginning of `Strategy.prototype.authenticate`.
*/
this._options = options;
this.name = 'azuread-openidconnect';
// stuff related to the verify function
this._verify = verify;
this._passReqToCallback = !!options.passReqToCallback;
if (options.useCookieInsteadOfSession === true)
this._useCookieInsteadOfSession = true;
else
this._useCookieInsteadOfSession = false;
if (!this._useCookieInsteadOfSession) {
// For each microsoft login page tab, we generate a {state, nonce, policy, timeStamp} tuple,
// normally user won't keep opening microsoft login page in new tabs without putting their
// password for more than 10 tabs, so we only keep the most recent 10 tuples in session.
// The lifetime of each tuple is 60 minutes or user specified.
this._sessionContentHandler = new SessionContentHandler(options.nonceMaxAmount || NONCE_MAX_AMOUNT, options.nonceLifetime ||
NONCE_LIFE_TIME);
} else {
this._cookieContentHandler = new CookieContentHandler(options.nonceMaxAmount || NONCE_MAX_AMOUNT, options.nonceLifetime || NONCE_LIFE_TIME
, options.cookieEncryptionKeys);
}
/* When a user is authenticated for the first time, passport adds a new field
* to req.session called 'passport', and puts a 'user' property inside (or your
* choice of field name and property name if you change passport._key and
* passport._userProperty values). req.session['passport']['user'] is usually
* user_id (or something similar) of the authenticated user to reduce the size
* of session. When the user logs out, req.session['passport']['user'] will be
* destroyed. Any request between login (when authenticated for the first time)
* and logout will have the 'user_id' in req.session['passport']['user'], so
* passport can fetch it, find the user object in database and put the user
* object into a new field: req.user. Then the subsequent middlewares and the
* app can use the user object. This is how passport keeps user authenticated.
*
* For state validation, we also take advantage of req.session. we create a new
* field: req.session[sessionKey], where the sessionKey is our choice (in fact,
* this._key, see below). When we send a request with state, we put state into
* req.session[sessionKey].state; when we expect a request with state in query
* or body, we compare the state in query/body with the one stored in
* req.session[sessionKey].state, and then destroy req.session[sessionKey].state.
* User can provide a state by using `authenticate(Strategy, {state: 'xxx'})`, or
* one will be generated automatically. This is essentially how passport-oauth2
* library does the state validation, and we use the same way in our library.
*
* request structure will look like the following. In real request some fields
* might not be there depending on the purpose of the request.
*
* request ---|--- sessionID
* |--- session --- |--- ...
* | |--- 'passport' ---| --- 'user': 'user_id etc'
* | |--- sessionKey---| --- state: 'xxx'
* |--- ...
* |--- 'user': full user info
*/
this._key = options.sessionKey || ('OIDC: ' + options.redirectUrl);
if (!options.identityMetadata) {
// default value should be https://login.microsoftonline.com/common/.well-known/openid-configuration
log.error('OIDCStrategy requires a metadata location that contains cert data for RSA and ECDSA callback.');
throw new TypeError(`OIDCStrategy requires a metadata location that contains cert data for RSA and ECDSA callback.`);
}
// if loggi ...n/a
function Strategy() {
}n/a
function authCodeFlowHandler(params, oauthConfig, optionsToValidate, req, next, iss, sub) {
/* we will do the following things in order:
* (1) use code to get id_token and access_token
* (2) validate the id_token and the access_token received
* (3) if user asks for userinfo and we are using AAD v1, then we use access_token to get
* userinfo, then make sure the userinfo has the same 'sub' as that in the 'id_token'
*/
const self = this;
var code = params.code;
log.info('entering Strategy.prototype._authCodeFlowHandler, received code: ' + code);
var issFromPrevIdToken = iss;
var subFromPrevIdToken = sub;
return self._getAccessTokenBySecretOrAssertion(code, oauthConfig, next, (getOAuthAccessTokenError, items) => {
if (getOAuthAccessTokenError)
return next(new Error(`In _authCodeFlowHandler: failed to redeem authorization code: ${aadutils.getErrorMessage(getOAuthAccessTokenError
)}`));
var access_token = items.access_token;
var refresh_token = items.refresh_token;
// id_token should be present
if (!items.id_token)
return next(new Error('In _authCodeFlowHandler: id_token is not received'));
// token_type must be 'Bearer' ignoring the case
if (items.token_type.toLowerCase() !== 'bearer') {
log.info('token_type received is: ', items.token_type);
return next(new Error(`In _authCodeFlowHandler: token_type received is not 'Bearer' ignoring the case`));
}
log.info('received id_token: ', items.id_token);
log.info('received access_token: ', access_token);
log.info('received refresh_token: ', refresh_token);
// validate id_token and access_token, so put them into params
params.access_token = access_token;
params.id_token = items.id_token;
return self._idTokenHandler(params, optionsToValidate, req, next, (jwtClaimsStr, jwtClaims) => {
// for 'code id_token' flow, check iss/sub in the id_token from the authorization endpoint
// with those in the id_token from token endpoint
if (issFromPrevIdToken && issFromPrevIdToken !== jwtClaims.iss)
return next(new Error('In _authCodeFlowHandler: After redeeming the code, iss in id_token from authorize_endpoint does not
match iss in id_token from token_endpoint'));
if (subFromPrevIdToken && subFromPrevIdToken !== jwtClaims.sub)
return next(new Error('In _authCodeFlowHandler: After redeeming the code, sub in id_token from authorize_endpoint does not
match sub in id_token from token_endpoint'));
const sub = jwtClaims.sub;
const iss = jwtClaims.iss;
// load the userinfo, if this is not v2 and if we don't specify the resource
// for v1 if we don't specify the resource, then the access_token we got is for userinfo endpoint, so we can use it to get
userinfo
if (!self._options._isV2 && !params.resource) {
// make sure we get an access_token
if (!access_token)
return next(new Error("In _authCodeFlowHandler: we want to access userinfo endpoint, but access_token is not received"));
let parsedUrl;
try {
parsedUrl = url.parse(oauthConfig.userinfo_endpoint, true);
} catch (urlParseException) {
return next(new Error(`In _authCodeFlowHandler: Failed to parse config property 'userInfoURL' with value ${oauthConfig
.userinfo_endpoint}`));
}
parsedUrl.query.schema = 'openid';
delete parsedUrl.search; // delete operations are slow; should we rather just overwrite it with {}
const userInfoURL = url.format(parsedUrl);
// ask oauth2 to use authorization header to bearer access token
var oauth2 = createOauth2Instance(oauthConfig);
oauth2.useAuthorizationHeaderforGET(true);
return oauth2.get(userInfoURL, access_token, (getUserInfoError, body) => {
if (getUserInfoError)
return next(new Error(`In _authCodeFlowHandler: failed to fetch user profile: ${aadutils.getErrorMessage(getUserInfoError
)}`)); ...n/a
function errorResponseHandler(err, err_description, next) {
const self = this;
log.info('Error received in the response was: ', err);
if (err_description)
log.info('Error description received in the response was: ', err_description);
// Unfortunately, we cannot return the 'error description' to the user, since
// it goes to http header by default and it usually contains characters that
// http header doesn't like, which causes the program to crash.
return next(new Error(err));
}n/a
function flowInitializationHandler(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.
const self = this;
log.info(`entering Strategy.prototype._flowInitializationHandler`);
const params = {
'redirect_uri': oauthConfig.redirectUrl,
'response_type': oauthConfig.responseType,
'response_mode': oauthConfig.responseMode,
'client_id': oauthConfig.clientID
};
log.info('We are sending the response_type: ', params.response_type);
log.info('We are sending the response_mode: ', params.response_mode);
if (oauthConfig.domain_hint)
params['domain_hint'] = oauthConfig.domain_hint;
if (oauthConfig.login_hint)
params['login_hint'] = oauthConfig.login_hint;
if (oauthConfig.prompt)
params['prompt'] = oauthConfig.prompt;
if (oauthConfig.extraAuthReqQueryParams) {
// copy the extra query parameters into params
for (var attributeName in oauthConfig.extraAuthReqQueryParams) {
if (oauthConfig.extraAuthReqQueryParams.hasOwnProperty(attributeName))
params[attributeName] = oauthConfig.extraAuthReqQueryParams[attributeName];
}
}
var policy = null;
if (self._options.isB2C) {
if (!req.query.p || req.query.p === '')
return next(new Error('In _flowInitializationHandler: missing policy in the request for B2C'));
// policy is not case sensitive. AAD turns policy to lower case.
policy = req.query.p.toLowerCase();
if (!policy || !CONSTANTS.POLICY_REGEX.test(policy))
return next(new Error(`In _flowInitializationHandler: the given policy ${policy} given in the request is invalid`));
}
// add state/nonce/policy/timeStamp tuple to session or cookie
let state = params.state = oauthConfig.customState ? ('CUSTOM' + aadutils.uid(32) + oauthConfig.customState) : aadutils.uid(32
);
let nonce = params.nonce = aadutils.uid(32);
let resource = oauthConfig.resource ? oauthConfig.resource : null;
if (resource)
params.resource = resource;
var tuple = { state: state, nonce: nonce };
if (policy)
tuple.policy = policy;
if (resource)
tuple.resource = resource;
if (oauthConfig.tenantIdOrName)
tuple.tenantIdOrName = oauthConfig.tenantIdOrName;
if (!self._useCookieInsteadOfSession) {
tuple.timeStamp = Date.now();
self._sessionContentHandler.add(req, self._key, tuple);
} else {
// we don't need to record timestamp if we use cookie, since we can set the
// expiration when we set cookie
self._cookieContentHandler.add(req, oauthConfig.response, tuple);
}
// add scope
params.scope = oauthConfig.scope;
// add telemetry
params[aadutils.getLibraryProductParameterName()] = aadutils.getLibraryProduct();
params[aadutils.getLibraryVersionParameterName()] = aadutils.getLibraryVersion();
let location;
// Implement support for standard OpenID Connect params (display, prompt, etc.)
if (self._options.isB2C)
location = `${oauthConfig.authorization_endpoint}&${querystring.stringify(params)}`;
else
location = `${oauthConfig.authorization_endpoint}?${querystring.stringify(params)}`;
return self.redirect(location);
}n/a
(code, oauthConfig, next, callback) => {
var post_headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
var post_params = {
code: code,
client_id: oauthConfig.clientID,
redirect_uri: oauthConfig.redirectUrl,
scope: oauthConfig.scope,
grant_type: 'authorization_code'
};
if (oauthConfig.extraTokenReqQueryParams) {
// copy the extra query parameters into post_params
for (var attributeName in oauthConfig.extraTokenReqQueryParams) {
if (oauthConfig.extraTokenReqQueryParams.hasOwnProperty(attributeName))
post_params[attributeName] = oauthConfig.extraTokenReqQueryParams[attributeName];
}
}
if (oauthConfig.clientSecret) {
// use client secret if it exists
post_params['client_secret'] = oauthConfig.clientSecret;
log.info('In _getAccessTokenBySecretOrAssertion: we are using client secret');
} else {
// otherwise generate a client assertion
post_params['client_assertion_type'] = CONSTANTS.CLIENT_ASSERTION_TYPE;
jwt.generateClientAssertion(oauthConfig.clientID, oauthConfig.token_endpoint, oauthConfig.privatePEMKey, oauthConfig.thumbprint
,
(err, assertion) => {
if (err)
return next(err);
else
post_params['client_assertion'] = assertion;
});
log.info('In _getAccessTokenBySecretOrAssertion: we created a client assertion with thumbprint ' + oauthConfig.thumbprint);
};
var post_data = querystring.stringify(post_params);
var oauth2 = createOauth2Instance(oauthConfig);
oauth2._request('POST', oauthConfig.token_endpoint, post_headers, post_data, null, (error, data, response) => {
if (error) callback(error);
else {
var results;
try {
results = JSON.parse(data);
} catch(e) {
results = querystring.parse(data);
}
callback(null, results);
}
});
}n/a
function hybridFlowHandler(params, oauthConfig, optionsToValidate, req, next) {
/* we will do the following things in order
* (1) validate the id_token and the code
* (2) if there is no userinfo token needed (or ignored if using AAD v2 ), we use
* the claims in id_token for user's profile
* (3) if userinfo token is needed, we will use the 'code' and the authorization code flow
*/
const self = this;
log.info('entering Strategy.prototype._hybridFlowHandler, received code: ' + params.code + ', received id_token: ' + params.id_token
);
// save nonce, since if we use the authorization code flow later, we have to check
// nonce again.
// validate the id_token and the code
return self._idTokenHandler(params, optionsToValidate, req, next, (jwtClaimsStr, jwtClaims) => {
// c_hash is required for 'code id_token' flow. If we have c_hash, then _validateResponse already
// validates it; otherwise, _validateResponse ignores the c_hash check, and we check here
if (!jwtClaims.c_hash)
return next(new Error("In _hybridFlowHandler: we are in hybrid flow using code id_token, but c_hash is not found in id_token
"));
const sub = jwtClaims.sub;
const iss = jwtClaims.iss;
// now we use the authorization code flow
return self._authCodeFlowHandler(params, oauthConfig, optionsToValidate, req, next, iss, sub);
});
}n/a
function idTokenHandler(params, optionsToValidate, req, next, callback) {
const self = this;
var id_token = params.id_token;
var parts = id_token.split('.');
if (parts.length === 3)
return self._validateResponse(params, optionsToValidate, req, next, callback);
else if (parts.length === 5) {
log.info('In _idTokenHandler: we received an id_token of JWE format, we are decrypting it');
var decrypted_token;
return jwe.decrypt(id_token, optionsToValidate.jweKeyStore, log, (err, decrypted_token) => {
if(err)
return next(err);
params.id_token = decrypted_token;
return self._validateResponse(params, optionsToValidate, req, next, callback);
});
} else
return next(new Error(`id_token has ${parts.length} parts, it is neither jwe nor jws`));
}n/a
function implicitFlowHandler(params, optionsToValidate, req, next) {
/* we will do the following things in order
* (1) validate id_token
* (2) use the claims in the id_token for user's profile
*/
const self = this;
log.info('entering Strategy.prototype._implicitFlowHandler, received id_token: ' + params.id_token);
// validate the id_token
return self._idTokenHandler(params, optionsToValidate, req, next, (jwtClaimsStr, jwtClaims) => {
const sub = jwtClaims.sub;
const iss = jwtClaims.iss;
log.info('we are in implicit flow, use the content in id_token as the profile');
return onProfileLoaded(self, {
req,
sub,
iss,
profile: makeProfileObject(jwtClaims, jwtClaimsStr),
jwtClaims,
access_token: null,
refresh_token: null,
params: null
});
});
}n/a
function validateResponse(params, optionsToValidate, req, next, callback) {
const self = this;
var id_token = params.id_token;
var code = params.code;
var access_token = params.access_token;
// decode id_token
const decoded = jws.decode(id_token);
if (decoded == null)
return next(new Error('In _validateResponse: Invalid JWT token'));
log.info('token decoded: ', decoded);
// get Pem Key
var PEMkey = null;
if (decoded.header.kid) {
PEMkey = params.metadata.generateOidcPEM(decoded.header.kid);
} else if (decoded.header.x5t) {
PEMkey = params.metadata.generateOidcPEM(decoded.header.x5t);
} else {
return next(new Error('In _validateResponse: We did not receive a token we know how to validate'));
}
log.info('PEMkey generated: ' + PEMkey);
// verify id_token signature and claims
return jwt.verify(id_token, PEMkey, optionsToValidate, (err, jwtClaims) => {
if (err)
return next(new Error(`In _validateResponse: ${aadutils.getErrorMessage(err)}`));
log.info("Claims received: ", jwtClaims);
// jwt checks the 'nbf', 'exp', 'aud', 'iss' claims
// there are a few other things we will check below
// For B2C, check the policy
if (self._options.isB2C) {
var policy_in_idToken;
if (jwtClaims.acr && CONSTANTS.POLICY_REGEX.test(jwtClaims.acr))
policy_in_idToken = jwtClaims.acr;
else if (jwtClaims.tfp && CONSTANTS.POLICY_REGEX.test(jwtClaims.tfp))
policy_in_idToken = jwtClaims.tfp.toLowerCase();
else
return next(new Error('In _validateResponse: invalid B2C policy in id_token'));
if (params.policy !== policy_in_idToken)
return next(new Error("In _validateResponse: policy in id_token does not match the policy used"));
}
// check nonce
if (!jwtClaims.nonce || jwtClaims.nonce === '' || jwtClaims.nonce !== optionsToValidate.nonce)
return next(new Error('In _validateResponse: invalid nonce'));
// check c_hash
if (jwtClaims.c_hash) {
// checkHashValueRS256 checks if code is null, so we don't bother here
if (!aadutils.checkHashValueRS256(code, jwtClaims.c_hash))
return next(new Error("In _validateResponse: invalid c_hash"));
}
// check at_hash
if (jwtClaims.at_hash) {
// checkHashValueRS256 checks if access_token is null, so we don't bother here
if (!aadutils.checkHashValueRS256(access_token, jwtClaims.at_hash))
return next(new Error("In _validateResponse: invalid at_hash"));
}
// return jwt claims and jwt claims string
var idTokenSegments = id_token.split('.');
var jwtClaimsStr = base64url.decode(idTokenSegments[1]);
return callback(jwtClaimsStr, jwtClaims);
});
}n/a
function authenticateStrategy(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;
// validate tenantIdOrName if it is provided
if (tenantIdOrName && !CONSTANTS.TENANTNAME_REGEX.test(tenantIdOrName) && !CONSTANTS.TENANTID_REGEX.test(tenantIdOrName))
return self.failWithLog(`In passport.authenticate: invalid tenantIdOrName ${tenantIdOrName}`);
// '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 'options ......
To complete the sample, provide a route that corresponds to the path
configuration parameter that is sent to the strategy:
```javascript
app.get('/login',
passport.authenticate('azuread-openidconnect', { failureRedirect: '/
x27; }),
function(req, res) {
log.info('Login was called in the Sample');
res.redirect('/');
});
// POST /auth/openid/return
// Use passport.authenticate() as route middleware to authenticate the
...collectInfoFromReq = function (params, req, next, response) {
const self = this;
// 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)))
return next(new Error('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 (req.query && (req.query.error || req.query.id_token || req.query.code))
source = req.query;
else if (req.body && (req.body.error || req.body.id_token || req.body.code))
source = req.body;
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'))
return next(new Error(`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)
return next(new Error('In collectInfoFromReq: missing state in the request'));
var tuple;
if (!self._useCookieInsteadOfSession)
tuple = self._sessionContentHandler.findAndDeleteTupleByState(req, self._key, params.state);
else
tuple = self._cookieContentHandler.findAndDeleteTupleByState(req, response, params.state);
if (!tuple)
return next(new Error('In collectInfoFromReq: invalid state received in the request'));
params.nonce = tuple['nonce'];
params.policy = tuple['policy'];
params.resource = tuple['resource'];
// user provided tenantIdOrName will be ignored for redirectUrl, since we saved the one we used in session
if (params.tenantIdOrName)
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) {
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))
return next(new Error('In collectInfoFromReq: issu ...n/a
failWithLog = function (message) {
this.log.info(`authentication failed due to: ${message}`);
return this.fail(message);
}n/a
function setOptions(params, oauthConfig, optionsToValidate, done) {
const self = this;
async.waterfall([
// ------------------------------------------------------------------------
// load metadata
// ------------------------------------------------------------------------
(next) => {
memoryCache.wrap(params.cachekey, (cacheCallback) => {
params.metadata.fetch((fetchMetadataError) => {
if (fetchMetadataError) {
return cacheCallback(new Error(`In setOptions: Unable to fetch metadata`));
}
return cacheCallback(null, params.metadata);
});
}, { ttl }, next);
},
// ------------------------------------------------------------------------
// set oauthConfig: the information we need for oauth flow like redeeming code/access_token
// ------------------------------------------------------------------------
(metadata, next) => {
if (!metadata.oidc)
return next(new Error('In setOptions: failed to load metadata'));
params.metadata = metadata;
// copy the fields needed into 'oauthConfig'
aadutils.copyObjectFields(metadata.oidc, oauthConfig, ['authorization_endpoint', 'token_endpoint', 'userinfo_endpoint']);
aadutils.copyObjectFields(self._options, oauthConfig, ['clientID', 'clientSecret', 'privatePEMKey', 'thumbprint', 'responseType
', 'responseMode', 'scope', 'redirectUrl']);
oauthConfig.tenantIdOrName = params.tenantIdOrName;
oauthConfig.extraAuthReqQueryParams = params.extraAuthReqQueryParams;
oauthConfig.extraTokenReqQueryParams = params.extraTokenReqQueryParams;
// validate oauthConfig
const validatorConfig = {
authorization_endpoint: Validator.isHttpsURL,
token_endpoint: Validator.isHttpsURL,
userinfo_endpoint: Validator.isHttpsURLIfExists,
};
try {
// validator will throw exception if a required option is missing
const checker = new Validator(validatorConfig);
checker.validate(oauthConfig);
} catch (ex) {
return next(new Error(`In setOptions: ${aadutils.getErrorMessage(ex)}`));
}
// for B2C, verify the endpoints in oauthConfig has the correct policy
if (self._options.isB2C){
var policyChecker = (endpoint, policy) => {
var u = {};
try {
u = url.parse(endpoint, true);
} catch (ex) {
}
return u.query && u.query.p && (policy.toLowerCase() === u.query.p.toLowerCase());
};
// B2C has no userinfo_endpoint, so no need to check it
if (!policyChecker(oauthConfig.authorization_endpoint, params.policy))
return next(new Error(`policy in ${oauthConfig.authorization_endpoint} should be ${params.policy}`));
if (!policyChecker(oauthConfig.token_endpoint, params.policy))
return next(new Error(`policy in ${oauthConfig.token_endpoint} should be ${params.policy}`));
}
return next(null, metadata);
},
// ------------------------------------------------------------------------
// set optionsToValidate: the information we need for id_token validation.
// we do this only if params has id_token or code, otherwise there is no
// id_token to validate
// ------------------------------------------------------------------------
(metadata, next) => {
if (!params.id_token && !params.code)
return next(null);
// set items from self._options
aadutils.copyObjectFields(self._options, optionsToValidate,
['validateIssuer', 'audience', 'allowMultiAudiencesInToken', 'ignoreExpiration', 'allowMultiAudiencesInToken']);
// algorithms
var algorithms = metadata.oidc.algorithms;
if (!algorithms)
return next(new Error('In setOptions: algorithms is missing in metadata'));
if (!Array.isArray(algorithms) || algorithms.length == 0 || (algorithms.length === 1 && algorithms[0] === 'no ...n/a