Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

passport-azure-ad

Package Overview
Dependencies
Maintainers
2
Versions
56
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

passport-azure-ad - npm Package Compare versions

Comparing version 3.0.0 to 3.0.1

lib/constants.js

30

CHANGELOG.md

@@ -1,2 +0,30 @@

<a name="3.0.0"></a>
<a name="3.0.1"></a>
# 3.0.1
## OIDCStrategy
### New features
* specify tenant per request
Now you can specify the tenant per request, using the `tenantIdOrName` option in `passport.authenticate`. More details on the usage can be found in README.md. `tenantIdOrName` enables two features:
* B2C common endpoint support
Now you can use the B2C common endpoint by specifying the tenant for each login request using the `tenantIdOrName` option. A login request is any request that doesn't contain code or id_token.
* extensive issuer validation on common endpoint
Previously, you had to provide an `issuer` value in configuration to validat the issuer on the common endpoint. Alternatively, you can now specify the tenant for each login request.
## Bug fixes
* [#239](https://github.com/AzureAD/passport-azure-ad/issues/239) Problems with signin in the updated sample
* [#233](https://github.com/AzureAD/passport-azure-ad/issues/233) Provide documentation with more details
* [#229](https://github.com/AzureAD/passport-azure-ad/issues/229) use tenant id dynamically for each request
* [#123](https://github.com/AzureAD/passport-azure-ad/issues/123) Question: what is the difference between OIDCStrategy and BearerStrategy. Which one should I use?
# 3.0.0

@@ -3,0 +31,0 @@

4

lib/jsonWebToken.js

@@ -144,3 +144,3 @@ /**

if (options.validateIssuer !== false) {
if (!options.issuer || options.issuer === '')
if (!options.issuer || options.issuer === '' || (Array.isArray(options.issuer) && options.issuer.length === 0))
return done(new Error('options.issuer is missing'));

@@ -192,3 +192,3 @@ var valid = false;

return done(new Error('invalid nbf value in payload'));
if (payload.nbf > Math.floor(Date.now() / 1000))
if (payload.nbf > Math.ceil(Date.now() / 1000) + 1) // add 1 more second to allow the clock screw
return done(new Error('jwt is not active'));

@@ -195,0 +195,0 @@ }

@@ -42,2 +42,3 @@ /**

const aadutils = require('./aadutils');
const CONSTANTS = require('./constants');
const jwt = require('./jsonWebToken');

@@ -71,5 +72,2 @@

// B2C policy is like 'b2c_1_policyname'. policy is not case sensitive.
const B2C_PREFIX = 'b2c_1_';
function makeProfileObject(src, raw) {

@@ -364,7 +362,2 @@ return {

// common endpoint is not allowed for B2C
if (options.isCommonEndpoint && options.isB2C) {
throw new Error(`Cannot use common endpoint for B2C. Please use your tenant name or tenant guid.`);
}
// add telemetry

@@ -376,6 +369,10 @@ options.identityMetadata = options.identityMetadata.concat(`?${aadutils.getLibraryProductParameterName()}=${aadutils.getLibraryProduct()}`)

* Take care of issuer and audience
* (1) We use the `issuer` if it is provided.
* If `issuer` is not provided, then
* for non-common endpoint, we use the issuer provided by metadata
* for common endpoint, we don't know the issuer, and `validateIssuer` must be set to false
* (1) We use user provided `issuer`, and the issuer value from metadata if the metadata
* comes from tenant-specific endpoint (in other words, either the identityMetadata
* is tenant-specific, or it is common but you provide tenantIdOrName in
* passport.authenticate).
*
* For common endpoint, if `issuer` is not provided by user, and `tenantIdOrName` is
* not used in passport.authenticate, then we don't know the issuer, and `validateIssuer`
* must be set to false
* (2) `validateIssuer` is true by default. we validate issuer unless validateIssuer is set false

@@ -386,12 +383,7 @@ * (3) `audience` must be the clientID of this app

options.validateIssuer = true;
if (!options.validateIssuer)
log.warn(`Production environments should always validate the issuer.`);
if (options.issuer === '')
options.issuer = null;
if (options.isCommonEndpoint && options.validateIssuer && !options.issuer)
throw new Error('we are using common endpoint, please either set validateIssuer to false or provide issuer in the configuration.');
if (!options.validateIssuer)
log.warn(`Production environments should always validate the issuer.`);
// make issuer an array

@@ -508,5 +500,10 @@ if (options.issuer && !Array.isArray(options.issuer))

var customState = options && options.customState;
var tenantIdOrName = options && options.tenantIdOrName;
// 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 = {};
var params = { 'tenantIdOrName': tenantIdOrName };
// 'oauthConfig': items needed for oauth flow (like redirection, code redemption), such as token_endpoint, userinfo_endpoint, etc

@@ -549,15 +546,15 @@ var oauthConfig = { 'resource': resource, 'customState': customState};

// handle the error
return self._errorResponseHandler(params.err, params.err_description);
return self._errorResponseHandler(params.err, params.err_description, next);
} else if (!params.id_token && !params.code) {
// ask for authorization, initialize the authorization process
return self._flowInitializationHandler(oauthConfig, req);
return self._flowInitializationHandler(oauthConfig, req, next);
} else if (params.id_token && params.code) {
// handle hybrid flow
return self._hybridFlowHandler(params, oauthConfig, optionsToValidate, req);
return self._hybridFlowHandler(params, oauthConfig, optionsToValidate, req, next);
} else if (params.id_token) {
// handle implicit flow
return self._implicitFlowHandler(params, optionsToValidate, req);
return self._implicitFlowHandler(params, optionsToValidate, req, next);
} else {
// handle authorization code flow
return self._authCodeFlowHandler(params, oauthConfig, optionsToValidate, req);
return self._authCodeFlowHandler(params, oauthConfig, optionsToValidate, req, next);
}

@@ -570,3 +567,3 @@ }

if (waterfallError) {
return self.failWithLog(`In authenticate: ${aadutils.getErrorMessage(waterfallError)}`);
return self.failWithLog(`${aadutils.getErrorMessage(waterfallError)}`);
}

@@ -595,3 +592,3 @@ return true;

(req.body && (req.body.access_token || req.body.refresh_token)))
return self.failWithLog('In collectInfoFromReq: neither access token nor refresh token is expected in the incoming request');
return next(new Error('In collectInfoFromReq: neither access token nor refresh token is expected in the incoming request'));

@@ -619,3 +616,3 @@ // -------------------------------------------------------------------------

if (!source.state.startsWith('CUSTOM'))
return self.failWithLog(`In collectInfoFromReq: invalid custom state ${state}`);
return next(new Error(`In collectInfoFromReq: invalid custom state ${state}`));

@@ -633,9 +630,13 @@ source.state = source.state.substring(38);

if (!params.state)
return self.failWithLog('In collectInfoFromReq: missing state in the request');
return next(new Error('In collectInfoFromReq: missing state in the request'));
var tuple = self._sessionContentHandler.findAndDeleteTupleByState(req, self._key, params.state);
if (!tuple)
return self.failWithLog('In collectInfoFromReq: invalid state received in the request');
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 {

@@ -645,23 +646,43 @@ 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: 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)
return self.failWithLog('In collectInfoFromReq: policy is missing');
return next(new Error('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)
return next(new Error('In collectInfoFromReq: we are using common endpoint for B2C but tenantIdOrName is not provided'));
// -------------------------------------------------------------------------
// create a cachekey and an Metadata object instance
// 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;
// -------------------------------------------------------------------------
var metadataUrl = self._options.identityMetadata;
if (self._options.isB2C) {
metadataUrl = metadataUrl.concat(`&p=${params.policy}`);
params.cachekey = 'policy: ' + params.policy; // B2C metadata cachekey
log.info(`B2C metadataUrl is: ${metadataUrl}`);
} else {
params.cachekey = 'ordinary'; // non B2C metadata cachekey
log.info(`metadataUrl is: ${metadataUrl}`);
// 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}/`);
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 = new Metadata(metadataUrl, 'oidc', self._options);
log.info(`metadataUrl is: ${metadataUrl}`);
log.info(`received the following items in params: ${JSON.stringify(params)}`);

@@ -691,3 +712,3 @@

if (fetchMetadataError) {
return self.failWithLog(`In setOptions: Unable to fetch metadata: ${aadutils.getErrorMessage(fetchMetadataError)}`);
return cacheCallback(new Error(`In setOptions: Unable to fetch metadata`));
}

@@ -704,3 +725,3 @@ return cacheCallback(null, params.metadata);

if (!metadata.oidc)
return self.failWithLog('In setOptions: failed to load metadata');
return next(new Error('In setOptions: failed to load metadata'));
params.metadata = metadata;

@@ -711,2 +732,3 @@

aadutils.copyObjectFields(self._options, oauthConfig, ['clientID', 'clientSecret', 'responseType', 'responseMode', 'scope', 'redirectUrl']);
oauthConfig.tenantIdOrName = params.tenantIdOrName;

@@ -725,3 +747,3 @@ // validate oauthConfig

} catch (ex) {
return self.failWithLog(`In setOptions: ${aadutils.getErrorMessage(ex)}`);
return next(new Error(`In setOptions: ${aadutils.getErrorMessage(ex)}`));
}

@@ -741,8 +763,8 @@

if (!policyChecker(oauthConfig.auth_endpoint, params.policy))
return self.failWithLog('policy in ${oauthConfig.auth_endpoint} should be ${params.policy}');
return next(new Error(`policy in ${oauthConfig.auth_endpoint} should be ${params.policy}`));
if (!policyChecker(oauthConfig.token_endpoint, params.policy))
return self.failWithLog('policy in ${oauthConfig.token_endpoint} should be ${params.policy}');
return next(new Error(`policy in ${oauthConfig.token_endpoint} should be ${params.policy}`));
}
next(null, metadata);
return next(null, metadata);
},

@@ -757,3 +779,3 @@

if (!params.id_token && !params.code)
next(null);
return next(null);

@@ -767,5 +789,5 @@ // set items from self._options

if (!algorithms)
return self.fail('In setOptions: algorithms is missing in metadata');
return next(new Error('In setOptions: algorithms is missing in metadata'));
if (!Array.isArray(algorithms) || algorithms.length == 0 || (algorithms.length === 1 && algorithms[0] === 'none'))
return self.fail('In setOptions: algorithms must be an array containing at least one algorithm');
return next(new Error('In setOptions: algorithms must be an array containing at least one algorithm'));
optionsToValidate.algorithms = algorithms;

@@ -777,8 +799,15 @@

// issuer
if (self._options.isCommonEndpoint)
optionsToValidate.issuer = self._options.issuer;
// if the metadata is not coming from common endpoint, we record the issuer value from metadata
if (!self._options.isCommonEndpoint || (self._options.isCommonEndpoint && params.tenantIdOrName))
optionsToValidate.issuer = [metadata.oidc.issuer];
else
optionsToValidate.issuer = self._options.issuer || metadata.oidc.issuer;
optionsToValidate.issuer = [];
// if user provided issuer, we also record these issuer values
if (self._options.issuer)
optionsToValidate.issuer = optionsToValidate.issuer.concat(self._options.issuer);
// if we don't get any issuer value and we want to validate issuer, we should fail
if (optionsToValidate.issuer.length === 0 && self._options.validateIssuer)
return next(new Error('In setOptions: we want to validate issuer but issuer is not found'));
next(null);
return next(null);
},

@@ -795,5 +824,6 @@ ], done);

* @param {Object} req
* @param {Function} next -- when error occurs, call next(err)
* @param {Function} callback
*/
Strategy.prototype._validateResponse = function validateResponse(params, optionsToValidate, req, callback) {
Strategy.prototype._validateResponse = function validateResponse(params, optionsToValidate, req, next, callback) {
const self = this;

@@ -808,3 +838,3 @@

if (decoded == null)
return self.failWithLog('In _validateResponse: Invalid JWT token');
return next(new Error('In _validateResponse: Invalid JWT token'));

@@ -820,3 +850,3 @@ log.info('token decoded: ', decoded);

} else {
return self.failWithLog('In _validateResponse: We did not receive a token we know how to validate');
return next(new Error('In _validateResponse: We did not receive a token we know how to validate'));
}

@@ -828,3 +858,3 @@ log.info('PEMkey generated: ' + PEMkey);

if (err)
return self.failWithLog(`In _validateResponse: ${aadutils.getErrorMessage(err)}`);
return next(new Error(`In _validateResponse: ${aadutils.getErrorMessage(err)}`));

@@ -838,7 +868,7 @@ log.info("Claims received: ", jwtClaims);

if (self._options.isB2C) {
if (!jwtClaims.acr || jwtClaims.acr.length <= B2C_PREFIX.length || jwtClaims.acr.substring(0,6).toLowerCase() !== B2C_PREFIX)
return self.failWithLog('In _validateResponse: invalid B2C policy in id_token');
if (!jwtClaims.acr || !CONSTANTS.POLICY_REGEX.test(jwtClaims.acr))
return next(new Error('In _validateResponse: invalid B2C policy in id_token'));
if (params.policy !== jwtClaims.acr)
return self.failWithLog("In _validateResponse: acr in id_token does not match the policy used");
return next(new Error("In _validateResponse: acr in id_token does not match the policy used"));
}

@@ -848,3 +878,3 @@

if (!jwtClaims.nonce || jwtClaims.nonce === '' || jwtClaims.nonce !== optionsToValidate.nonce)
return self.failWithLog('In _validateResponse: invalid nonce');
return next(new Error('In _validateResponse: invalid nonce'));

@@ -855,3 +885,3 @@ // check c_hash

if (!aadutils.checkHashValueRS256(code, jwtClaims.c_hash))
return self.failWithLog("In _validateResponse: invalid c_hash");
return next(new Error("In _validateResponse: invalid c_hash"));
}

@@ -863,3 +893,3 @@

if (!aadutils.checkHashValueRS256(access_token, jwtClaims.at_hash))
return self.failWithLog("In _validateResponse: invalid at_hash");
return next(new Error("In _validateResponse: invalid at_hash"));
}

@@ -879,4 +909,5 @@

* @params {String} err_description
* @params {Function} next -- callback to pass error to async.waterfall
*/
Strategy.prototype._errorResponseHandler = function errorResponseHandler(err, err_description) {
Strategy.prototype._errorResponseHandler = function errorResponseHandler(err, err_description, next) {
const self = this;

@@ -891,3 +922,3 @@

// http header doesn't like, which causes the program to crash.
return self.failWithLog(err);
return next(new Error(err));
};

@@ -901,4 +932,5 @@

* @params {Object} req
* @params {Function} next -- callback to pass error to async.waterfall
*/
Strategy.prototype._implicitFlowHandler = function implicitFlowHandler(params, optionsToValidate, req) {
Strategy.prototype._implicitFlowHandler = function implicitFlowHandler(params, optionsToValidate, req, next) {
/* we will do the following things in order

@@ -914,3 +946,3 @@ * (1) validate id_token

// validate the id_token
return self._validateResponse(params, optionsToValidate, req, (jwtClaimsStr, jwtClaims) => {
return self._validateResponse(params, optionsToValidate, req, next, (jwtClaimsStr, jwtClaims) => {
const sub = jwtClaims.sub;

@@ -941,4 +973,5 @@ const iss = jwtClaims.iss;

* @params {Object} req
* @params {Function} next -- callback to pass error to async.waterfall
*/
Strategy.prototype._hybridFlowHandler = function hybridFlowHandler(params, oauthConfig, optionsToValidate, req) {
Strategy.prototype._hybridFlowHandler = function hybridFlowHandler(params, oauthConfig, optionsToValidate, req, next) {
/* we will do the following things in order

@@ -958,7 +991,7 @@ * (1) validate the id_token and the code

// validate the id_token and the code
return self._validateResponse(params, optionsToValidate, req, (jwtClaimsStr, jwtClaims) => {
return self._validateResponse(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 self.failWithLog("In _hybridFlowHandler: we are in hybrid flow using code id_token, but c_hash is not found in id_token");
return next(new Error("In _hybridFlowHandler: we are in hybrid flow using code id_token, but c_hash is not found in id_token"));

@@ -969,3 +1002,3 @@ const sub = jwtClaims.sub;

// now we use the authorization code flow
return self._authCodeFlowHandler(params, oauthConfig, optionsToValidate, req, iss, sub);
return self._authCodeFlowHandler(params, oauthConfig, optionsToValidate, req, next, iss, sub);
});

@@ -981,2 +1014,3 @@ };

* @params {Object} req
* @params {Function} next -- callback to pass error to async.waterfall
* // the following are required if you used 'code id_token' flow then call this function to

@@ -989,3 +1023,3 @@ * // redeem the code for another id_token from the token endpoint. iss and sub are those

*/
Strategy.prototype._authCodeFlowHandler = function authCodeFlowHandler(params, oauthConfig, optionsToValidate, req, iss, sub) {
Strategy.prototype._authCodeFlowHandler = function authCodeFlowHandler(params, oauthConfig, optionsToValidate, req, next, iss, sub) {
/* we will do the following things in order:

@@ -1026,7 +1060,7 @@ * (1) use code to get id_token and access_token

if (getOAuthAccessTokenError)
return self.failWithLog(`In _authCodeFlowHandler: failed to redeem authorization code: ${aadutils.getErrorMessage(getOAuthAccessTokenError)}`);
return next(new Error(`In _authCodeFlowHandler: failed to redeem authorization code: ${aadutils.getErrorMessage(getOAuthAccessTokenError)}`));
// id_token should be present
if (!items.id_token)
return self.failWithLog('In _authCodeFlowHandler: id_token is not received');
return next(new Error('In _authCodeFlowHandler: id_token is not received'));

@@ -1036,3 +1070,3 @@ // token_type must be 'Bearer'

log.info('token_type received is: ', items.token_type);
return self.failWithLog(`In _authCodeFlowHandler: token_type received is not 'Bearer'`);
return next(new Error(`In _authCodeFlowHandler: token_type received is not 'Bearer'`));
}

@@ -1048,9 +1082,9 @@

return self._validateResponse(params, optionsToValidate, req, (jwtClaimsStr, jwtClaims) => {
return self._validateResponse(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 self.failWithLog('In _authCodeFlowHandler: After redeeming the code, iss in id_token from authorize_endpoint does not match iss in id_token from token_endpoint');
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 self.failWithLog('In _authCodeFlowHandler: After redeeming the code, sub in id_token from authorize_endpoint does not match sub in id_token from token_endpoint');
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'));

@@ -1065,3 +1099,3 @@ const sub = jwtClaims.sub;

if (!access_token)
return self.failWithLog("In _authCodeFlowHandler: we want to access userinfo endpoint, but access_token is not received");
return next(new Error("In _authCodeFlowHandler: we want to access userinfo endpoint, but access_token is not received"));

@@ -1072,3 +1106,3 @@ let parsedUrl;

} catch (urlParseException) {
return self.failWithLog(`In _authCodeFlowHandler: Failed to parse config property 'userInfoURL' with value ${oauthConfig.userinfo_endpoint}`);
return next(new Error(`In _authCodeFlowHandler: Failed to parse config property 'userInfoURL' with value ${oauthConfig.userinfo_endpoint}`));
}

@@ -1084,3 +1118,3 @@

if (getUserInfoError)
return self.failWithLog(`In _authCodeFlowHandler: failed to fetch user profile: ${aadutils.getErrorMessage(getUserInfoError)}`);
return next(new Error(`In _authCodeFlowHandler: failed to fetch user profile: ${aadutils.getErrorMessage(getUserInfoError)}`));

@@ -1094,3 +1128,3 @@ log.info('Profile loaded from MS identity', body);

} catch (ex) {
return self.failWithLog(`In _authCodeFlowHandler: failed to parse userinfo ${body}, due to ${aadutils.getErrorMessage(ex)}`);
return next(new Error(`In _authCodeFlowHandler: failed to parse userinfo ${body}, due to ${aadutils.getErrorMessage(ex)}`));
}

@@ -1100,3 +1134,3 @@

if (userinfoReceived.sub !== jwtClaims.sub)
return self.failWithLog('In _authCodeFlowHandler: sub received in userinfo and id_token do not match');
return next(new Error('In _authCodeFlowHandler: sub received in userinfo and id_token do not match'));

@@ -1138,4 +1172,5 @@ return onProfileLoaded(self, {

* @params {Object} req
* @params {Function} next -- callback to pass error to async.waterfall
*/
Strategy.prototype._flowInitializationHandler = function flowInitializationHandler(oauthConfig, req) {
Strategy.prototype._flowInitializationHandler = function flowInitializationHandler(oauthConfig, req, next) {
// The request being authenticated is initiating OpenID Connect

@@ -1164,7 +1199,7 @@ // authentication. Prior to redirecting to the provider, configuration will

if (!req.query.p || req.query.p === '')
return self.failWithLog('In _flowInitializationHandler: missing policy in the request for B2C');
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.startsWith(B2C_PREFIX))
return self.failWithLog(`In _flowInitializationHandler: the given policy ${policy} given in the request is invalid`);
if (!policy || !CONSTANTS.POLICY_REGEX.test(policy))
return next(new Error(`In _flowInitializationHandler: the given policy ${policy} given in the request is invalid`));
}

@@ -1177,3 +1212,3 @@ // add state/nonce/policy/timeStamp tuple to session

params.resource = resource;
self._sessionContentHandler.add(req, self._key, {state: state, nonce: nonce, policy: policy, resource: resource, timeStamp: Date.now()});
self._sessionContentHandler.add(req, self._key, {state: state, nonce: nonce, policy: policy, resource: resource, tenantIdOrName: oauthConfig.tenantIdOrName, timeStamp: Date.now()});

@@ -1180,0 +1215,0 @@ // add scope

{
"name": "passport-azure-ad",
"version": "3.0.0",
"version": "3.0.1",
"license": "MIT",

@@ -5,0 +5,0 @@ "keywords": [

@@ -15,11 +15,11 @@

## Security Vulnerability in Versions < 1.4.6 and 2.0.0
## 1. Security Vulnerability in Versions < 1.4.6 and 2.0.0
_passport-azure-ad_ has a known security vulnerability affecting versions <1.4.6 and 2.0.0. Please update to >=1.4.6 or >=2.0.1 immediately. For more details, see the [security notice](https://github.com/AzureAD/passport-azure-ad/blob/master/SECURITY-NOTICE.MD).
## Versions
Current version - 3.0.0
## 2. Versions
Current version - 3.0.1
Minimum recommended version - 1.4.6
You can find the changes for each version in the [change log](https://github.com/AzureAD/passport-azure-ad/blob/master/CHANGELOG.md).
## Contribution History
## 3. Contribution History

@@ -30,52 +30,25 @@ [![Stories in Ready](https://badge.waffle.io/AzureAD/passport-azure-ad.png?label=ready&title=Ready)](https://waffle.io/AzureAD/passport-azure-ad)

## Installation
## 4. Installation
$ npm install passport-azure-ad
## Usage
## 5. Usage
### Configure strategy
This library contains two strategies: OIDCStrategy and BearerStrategy.
This sample uses the OAuth2Bearer Strategy:
OIDCStrategy uses OpenID Connect protocol for web application login purposes. It works in the following manner:
If a user is not logged in, passport sends an authentication request to AAD (Azure Active Directory), and AAD prompts the user for his or her sign-in credentials. On successful authentication, depending on the flow you choose, web application will eventually get an id_token back either directly from the AAD authorization endpoint or by redeeming a code at the AAD token endpoint. Passport then validates the id_token and propagates the claims in id_token back to the verify callback, and let the framework finish the remaining authentication procedure. If the whole process is successful, passport adds the user information to `req.user` and passes it to the next middleware. In case of error, passport either sends back an unauthorized response or redirects the user to the page you specified (such as homepage or login page).
```javascript
BearerStrategy uses Bearer Token protocol to protect web resource/api. It works in the following manner:
User sends a request to the protected web api which contains an access_token in either the authorization header or body. Passport extracts and validates the access_token, and propagates the claims in access_token to the verify callback and let the framework finish the remaining authentication procedure. On successful authentication, passport adds the user information to `req.user` and passes it to the next middleware, which is usually the business logic of the web resource/api. In case of error, passport sends back an unauthorized response.
// We pass these options in to the ODICBearerStrategy.
var options = {
identityMetadata: config.creds.identityMetadata,
clientID: config.creds.clientID,
validateIssuer: config.creds.validateIssuer,
issuer: config.creds.issuer,
passReqToCallback: config.creds.passReqToCallback,
isB2C: config.creds.isB2C,
policyName: config.creds.policyName,
allowMultiAudiencesInToken: config.creds.allowMultiAudiencesInToken,
audience: config.creds.audience,
loggingLevel: config.creds.loggingLevel,
};
We support AAD v1, v2 and B2C tenants for both strategies. Please check out section 7 for the samples. You can manage v1 tenants and register applications at https://manage.windowsazure.com. For v2 tenants and applications, you should go to https://apps.dev.microsoft.com. For B2C tenants, go to https://manage.windowsazure.com and click 'Manage B2C settings' to register applications and policies.
var bearerStrategy = new BearerStrategy(options,
function(token, done) {
log.info('verifying the user');
log.info(token, 'was the token retreived');
findById(token.oid, function(err, user) {
if (err) {
return done(err);
}
if (!user) {
// "Auto-registration"
log.info('User was added automatically as they were new. Their oid is: ', token.oid);
users.push(token);
owner = token.oid;
return done(null, token);
}
owner = token.oid;
return done(null, user, token);
});
}
);
```
This sample uses the OIDCStrategy:
### 5.1 OIDCStrategy
#### 5.1.1 Configure strategy and provide callback function
##### 5.1.1.1 Sample using the OIDCStrategy
```javascript

@@ -120,4 +93,106 @@ passport.use(new OIDCStrategy({

### Provide the authentication callback for OIDCStrategy
##### 5.1.1.2 Options
* `identityMetadata` (Required)
The metadata endpoint provided by the Microsoft Identity Portal that provides the keys and other important information at runtime. Examples:
* v1 tenant-specific endpoint
```
https://login.microsoftonline.com/your_tenant_name.onmicrosoft.com/.well-known/openid-configuration
https://login.microsoftonline.com/your_tenant_guid/.well-known/openid-configuration
```
* v1 common endpoint
```
https://login.microsoftonline.com/common/.well-known/openid-configuration
```
* v2 tenant-specific endpoint
```
https://login.microsoftonline.com/your_tenant_name.onmicrosoft.com/v2.0/.well-known/openid-configuration
https://login.microsoftonline.com/your_tenant_guid/v2.0/.well-known/openid-configuration
```
* v2 common endpoint
```
https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
```
For B2C, you cannot use v2 common endpoint unless you specify the tenant in `passport.authenticate` using `tenantIdOrName` option. See section 5.1.3 for more details.
* `clientID` (Required)
The client ID of your application in AAD (Azure Active Directory)
* `responseType` (Required)
Must be 'code', 'code id_token', 'id_token code' or 'id_token'. For login only flows you can use 'id_token'; if you want access_token, use 'code', 'code id_token' or 'id_token code'.
* `responseMode` (Required)
Must be 'query' or 'form_post'. This is how you get code or id_token back. 'form_post' is recommended for all scenarios.
* `redirectUrl` (Required)
Must be a https url string, unless you set `allowHttpForRedirectUrl` to true. This is the reply URL registered in AAD for your app. Production environment should always use https for `redirectUrl`.
* `passReqToCallback` (Required)
Whether you want to use `req` as the first parameter in the verify callback. See section 5.1.1.3 for more details.
* `allowHttpForRedirectUrl` (Conditional)
Required to set to true if you want to use http url for redirectUrl like `http://localhost:3000`.
* `clientSecret` (Conditional)
Required if the `responseType` is not 'id_token'. This is the app key of your app in AAD. For B2C, the app key sometimes contains \, please replace \ with two \'s in the app key, otherwise \ will be treated as the beginning of an escaping character.
* `isB2C` (Conditional)
Required to set to true if you are using B2C tenant.
* `validateIssuer` (Conditional)
Required to set to false if you don't want to validate issuer, default value is true. We validate the `iss` claim in id_token against user provided `issuer` values and the issuer value we get from tenant-specific endpoint. If you use common endpoint for `identityMetadata` and you want to validate issuer, then you have to either provide `issuer`, or provide the tenant for each login request using `tenantIdOrName` option in `passport.authenticate` (see section 5.1.3 for more details).
* `issuer` (Conditional)
This can be a string or an array of strings. See `validateIssuer` for the situation that requires `issuer`.
* `scope` (Optional)
List of scope values besides `openid` indicating the required scope of the access token for accessing the requested resource. For example, ['email', 'profile']. If you need refresh_token for v2 endpoint, then you have to include the 'offline_access' scope.
* `loggingLevel` (Optional)
Logging level. 'info', 'warn' or 'error'.
* `nonceLifetime` (Optional)
The lifetime of nonce in session in seconds. The default value is 3600 seconds.
##### 5.1.1.3 Verify callback
If you set `passReqToCallback` option to false, you can use one of the following signatures for the verify callback
```
function(iss, sub, profile, jwtClaims, access_token, refresh_token, params, done)
function(iss, sub, profile, access_token, refresh_token, params, done)
function(iss, sub, profile, access_token, refresh_token, done)
function(iss, sub, profile, done)
function(iss, sub, done)
function(profile, done)
```
If you set `passReqToCallback` option to true, you can use one of the following signatures for the verify callback
```
function(req, iss, sub, profile, jwtClaims, access_token, refresh_token, params, done)
function(req, iss, sub, profile, access_token, refresh_token, params, done)
function(req, iss, sub, profile, access_token, refresh_token, done)
function(req, iss, sub, profile, done)
function(req, iss, sub, done)
function(req, profile, done)
```
#### 5.1.2 Use `passport.authenticate` to protect routes
To complete the sample, provide a route that corresponds to the path

@@ -142,8 +217,7 @@ configuration parameter that is sent to the strategy:

passport.authenticate('azuread-openidconnect', { failureRedirect: '/' }),
function(req, res) {
function(req, res) {
res.redirect('/');
});
app.get('/logout', function(req, res){
app.get('/logout', function(req, res){
req.logout();

@@ -155,9 +229,7 @@ res.redirect('/');

### Options available for `passport.authenticate`
#### 5.1.3 Options available for `passport.authenticate`
#### OIDCStrategy
* `failureRedirect`: the url redirected to when the authentication fails
* `session`: if you don't want a persistent login session, you can use `session: false`
* `session`: if you don't want a persistent login session, you can use `session: false`. The default value is true.

@@ -169,2 +241,7 @@ * `customState`: if you want to use a custom state value instead of a random generated one

* `tenantIdOrName`: if you want to specify the tenant to use for this request. You can use the tenant guid or tenant name (like 'contoso.onmicrosoft.com'). Note:
* You must use common endpoint for `identityMetadata`, otherwise this option will be ignored. We will fetch and use the metadata from the tenant you specify for this request.
* This option only applies to the login request, in other words, the request which is not supposed to contain code or id_token. Passport saves the `tenantIdOrName` value in session before sending the authentication request. When we receive a request containing code or id_token, we retrieve the saved `tenantIdOrName` value from session and use that value.
* If you are using B2C common endpoint, then `tenantIdOrName` must be used for every login request.
Example:

@@ -174,8 +251,139 @@

passport.authenticate('azuread-openidconnect', { failureRedirect: '/', session: false, customState: 'my_state', resourceURL: 'https://graph.microsoft.com/mail.send'});
passport.authenticate('azuread-openidconnect', { tenantIdOrName: 'contoso.onmicrosoft.com' });
```
#### BearerStrategy
### 5.2 BearerStrategy
* `session`: if you don't want a persistent login session, you can use `session: false`
#### 5.2.1 Configure strategy and provide callback function
##### 5.2.1.1 Sample using the BearerStrategy
```javascript
// We pass these options in to the ODICBearerStrategy.
var options = {
identityMetadata: config.creds.identityMetadata,
clientID: config.creds.clientID,
validateIssuer: config.creds.validateIssuer,
issuer: config.creds.issuer,
passReqToCallback: config.creds.passReqToCallback,
isB2C: config.creds.isB2C,
policyName: config.creds.policyName,
allowMultiAudiencesInToken: config.creds.allowMultiAudiencesInToken,
audience: config.creds.audience,
loggingLevel: config.creds.loggingLevel,
};
var bearerStrategy = new BearerStrategy(options,
function(token, done) {
log.info('verifying the user');
log.info(token, 'was the token retreived');
findById(token.oid, function(err, user) {
if (err) {
return done(err);
}
if (!user) {
// "Auto-registration"
log.info('User was added automatically as they were new. Their oid is: ', token.oid);
users.push(token);
owner = token.oid;
return done(null, token);
}
owner = token.oid;
return done(null, user, token);
});
}
);
```
##### 5.2.1.2 Options
* `identityMetadata` (Required)
The metadata endpoint provided by the Microsoft Identity Portal that provides the keys and other important information at runtime. Examples:
* v1 tenant-specific endpoint
```
https://login.microsoftonline.com/your_tenant_name.onmicrosoft.com/.well-known/openid-configuration
https://login.microsoftonline.com/your_tenant_guid/.well-known/openid-configuration
```
* v1 common endpoint
```
https://login.microsoftonline.com/common/.well-known/openid-configuration
```
* v2 tenant-specific endpoint
```
https://login.microsoftonline.com/your_tenant_name.onmicrosoft.com/v2.0/.well-known/openid-configuration
https://login.microsoftonline.com/your_tenant_guid/v2.0/.well-known/openid-configuration
```
* v2 common endpoint
```
https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
```
For B2C, you can only use v2 tenant-specific endpoint.
* `clientID` (Required)
The client ID of your application in AAD (Azure Active Directory)
* `passReqToCallback` (Required)
Whether you want to use `req` as the first parameter in the verify callback. See section 5.2.1.3 for more details.
* `isB2C` (Conditional)
Required to set to true if you are using B2C tenant.
* `policyName` (Conditional)
Required if you are using B2C tenant. It is a string starting with 'B2C_1_' (case insensitive).
* `validateIssuer` (Conditional)
Required to set to false if you don't want to validate issuer, default value is true. We validate the `iss` claim in id_token against user provided `issuer` values and the issuer value we get from tenant-specific endpoint. If you use common endpoint for `identityMetadata` and you want to validate issuer, then you must provide `issuer`.
* `issuer` (Conditional)
This can be a string or an array of strings. See `validateIssuer` for the situation that requires `issuer`.
* `allowMultiAudiencesInToken` (Conditional)
Required if you allow access_token whose `aud` claim contains multiple values.
* `audience` (Optional)
Must be a string or an array of strings. We invalidate the `aud` claim in access_token against `audience`. The default value for `audience` is `clientID`.
* `loggingLevel` (Optional)
Logging level. 'info', 'warn' or 'error'.
##### 5.2.1.3 Verify callback
If you set `passReqToCallback` option to false, you can use the following verify callback
```
function(token, done)
```
If you set `passReqToCallback` option to true, you can use the following verify callback
```
function(req, token, done)
```
#### 5.2.2 Use `passport.authenticate` to protect resources or APIs
In the following example, we are using passport to protect '/api/tasks'. User sends a GET request to '/api/tasks' with access_token in authorization header or body. Passport validates the access_token, adds the related claims from access_token to `req.user`, and passes the request to listTasks middleware. The listTasks middleware can then read the user information in `req.user` and list all the tasks related to this user. Note that we do authentication every time, so we don't need to keep this user in session, and this can be achieved by using `session: false` option.
```javascript
server.get('/api/tasks', passport.authenticate('oauth-bearer', { session: false }), listTasks);
```
#### 5.2.3 Options available for `passport.authenticate`
* `session`: if you don't want a persistent login session, you can use `session: false`. The default value is true.
Example:

@@ -187,3 +395,3 @@

## Test
## 6. Test

@@ -204,3 +412,3 @@ In the library root folder, type the following command to install the dependency packages:

## Samples and Documentation
## 7. Samples and Documentation

@@ -215,3 +423,3 @@ [We provide a full suite of sample applications and documentation on GitHub](https://azure.microsoft.com/en-us/documentation/samples/?service=active-directory)

### Samples for [OpenID connect strategy](https://github.com/AzureAD/passport-azure-ad/blob/master/lib/oidcstrategy.js)
### 7.1 Samples for [OpenID connect strategy](https://github.com/AzureAD/passport-azure-ad/blob/master/lib/oidcstrategy.js)

@@ -224,3 +432,3 @@ * [sample using v1 endpoint](https://github.com/AzureADQuickStarts/WebApp-OpenIDConnect-NodeJS)

### Samples for [Bearer strategy](https://github.com/AzureAD/passport-azure-ad/blob/master/lib/bearerstrategy.js)
### 7.2 Samples for [Bearer strategy](https://github.com/AzureAD/passport-azure-ad/blob/master/lib/bearerstrategy.js)

@@ -233,3 +441,3 @@ * [sample using v1 endpoint](https://github.com/AzureADQuickStarts/WebAPI-Bearer-NodeJS)

## Community Help and Support
## 8. Community Help and Support

@@ -240,7 +448,7 @@ We leverage [Stack Overflow](http://stackoverflow.com/) to work with the community on supporting Azure Active Directory and its SDKs, including this one. We highly recommend you ask your questions on Stack Overflow (we're all on there!) Also browser existing issues to see if someone has had your question before.

## Security Reporting
## 9. Security Reporting
If you find a security issue with our libraries or services please report it to [secure@microsoft.com](mailto:secure@microsoft.com) with as much detail as possible. Your submission may be eligible for a bounty through the [Microsoft Bounty](http://aka.ms/bugbounty) program. Please do not post security issues to GitHub Issues or any other public site. We will contact you shortly upon receiving the information. We encourage you to get notifications of when security incidents occur by visiting [this page](https://technet.microsoft.com/en-us/security/dd252948) and subscribing to Security Advisory Alerts.
## Contributing
## 10. Contributing

@@ -251,14 +459,17 @@ All code is licensed under the MIT license and we triage actively on GitHub. We enthusiastically welcome contributions and feedback. You can clone the repo and start contributing now.

## Versions
Please check the releases for updates.
## 11. Releases
## Acknowledgements
Please check the [releases](https://github.com/AzureAD/passport-azure-ad/releases) for updates.
## 12. Acknowledgements
The code is based on Henri Bergius's [passport-saml](https://github.com/bergie/passport-saml) library and Matias Woloski's [passport-wsfed-saml2](https://github.com/auth0/passport-wsfed-saml2) library as well as Kiyofumi Kondoh's [passport-openid-google](https://github.com/kkkon/passport-google-openidconnect).
## License
## 13. License
Copyright (c) Microsoft Corp. All rights reserved. Licensed under the MIT License;
## We Value and Adhere to the Microsoft Open Source Code of Conduct
## 14. Microsoft Open Source Code of Conduct
We Value and Adhere to the Microsoft Open Source Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

@@ -42,3 +42,10 @@ /**

// Mock the process of getting PEMkey
var PEMkey = "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAs4W7xjkQZP3OwG7PfRgcYKn8eRYXHiz1iK503fS+K2FZo+Ublwwa\n2xFZWpsUU/jtoVCwIkaqZuo6xoKtlMYXXvfVHGuKBHEBVn8b8x/57BQWz1d0KdrN\nXxuMvtFe6RzMqiMqzqZrzae4UqVCkYqcR9gQx66Ehq7hPmCxJCkg7ajo7fu6E7dP\nd34KH2HSYRsaaEA/BcKTeb9H1XE/qEKjog68wUU9Ekfl3FBIRN+1Ah/BoktGFoXy\ni/jt0+L0+gKcL1BLmUlGzMusvRbjI/0+qj+mc0utGdRjY+xIN2yBj8vl4DODO+wM\nwfp+cqZbCd9TENyHaTb8iA27s+73L3ExOQIDAQAB\n-----END RSA PUBLIC KEY-----\n";
var PEMkey = "-----BEGIN RSA PUBLIC KEY-----\n\
MIIBCgKCAQEAs4W7xjkQZP3OwG7PfRgcYKn8eRYXHiz1iK503fS+K2FZo+Ublwwa\n\
2xFZWpsUU/jtoVCwIkaqZuo6xoKtlMYXXvfVHGuKBHEBVn8b8x/57BQWz1d0KdrN\n\
XxuMvtFe6RzMqiMqzqZrzae4UqVCkYqcR9gQx66Ehq7hPmCxJCkg7ajo7fu6E7dP\n\
d34KH2HSYRsaaEA/BcKTeb9H1XE/qEKjog68wUU9Ekfl3FBIRN+1Ah/BoktGFoXy\n\
i/jt0+L0+gKcL1BLmUlGzMusvRbjI/0+qj+mc0utGdRjY+xIN2yBj8vl4DODO+wM\n\
wfp+cqZbCd9TENyHaTb8iA27s+73L3ExOQIDAQAB\n\
-----END RSA PUBLIC KEY-----\n";

@@ -45,0 +52,0 @@ /*

@@ -360,34 +360,14 @@ /**

'validateIssuer tests on v2 common endpoint': (test) => {
test.expect(7);
test.expect(5);
setConfigCommon('https://www.example.com', '123', 'id_token code', 'form_post', true, null, (oidcConfig) =>
{
test.throws(() => {
test.doesNotThrow(() => {
new OidcStrategy(oidcConfig, noop);
},
Error,
'Should throw with validateIssuer set true on common endpoint without issuer provided'
'Should not throw with validateIssuer set true on common endpoint'
);
});
setConfigCommon('https://www.example.com', '123', 'id_token code', 'form_post', null, null, (oidcConfig) =>
{
test.throws(() => {
new OidcStrategy(oidcConfig, noop);
},
Error,
'Should throw with the default validateIssuer value on common endpoint without issuer provided'
);
});
setConfigCommon('https://www.example.com', '123', 'id_token code', 'form_post', undefined, null, (oidcConfig) =>
{
test.throws(() => {
new OidcStrategy(oidcConfig, noop);
},
Error,
'Should throw with the default validateIssuer value on common endpoint without issuer provided'
);
});
setConfigCommon('https://www.example.com', '123', 'id_token code', 'form_post', true, 'my_issuer', (oidcConfig) =>

@@ -394,0 +374,0 @@ {

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc