This repository has been archived by the owner on Aug 28, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 175
Issue #231 Support client_assertion for OIDC auth flow #275
Merged
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -189,13 +189,28 @@ function onProfileLoaded(strategy, args) { | |
* The default value is false. It's OK to use http like 'http://localhost:3000' in the | ||
* dev environment, but in production environment https should always be used. | ||
* | ||
* - `clientSecret` (1) Required if `responseType` is 'code', 'id_token code' or 'code id_token' | ||
* - `clientSecret` (1) This option only applies when `responseType` is 'code', 'id_token code' or 'code id_token'. | ||
* To redeem an authorization code, we can use either client secret flow or client assertion flow. | ||
* (1.1) For B2C, clientSecret is required since client assertion is not supported | ||
* (1.2) For non-B2C, both flows are supported. Developer must provide either clientSecret, or | ||
* thumbprint and privatePEMKey. We use clientSecret if it is provided, otherwise we use | ||
* thumbprint and privatePEMKey for the client assertion flow. | ||
* (2) must be a string | ||
* (3) Description: | ||
* The app key of your app from AAD. | ||
* NOTE: For B2C, the app key sometimes contains '\', please replace '\' with '\\' in the app key, otherwise | ||
* '\' will be treated as the beginning of a escaping character | ||
* | ||
* - `thumbprint` (1) Required if you want to use client assertion to redeem an authorization code (non-B2C only) | ||
* (2) must be a base64url encoded string | ||
* (3) Description: | ||
* The thumbprint (hash value) of the public key | ||
* | ||
* - `privatePEMKey` (1) Required if you want to use client assertion to redeem an authorization code (non-B2C only) | ||
* (2) must be a pem key | ||
* (3) Description: | ||
* The private key used to sign the client assertion JWT | ||
* | ||
* - `isB2C` (1) Required for B2C | ||
* (2) must be true for B2C, default is false | ||
* (3) Description: | ||
|
@@ -251,6 +266,8 @@ function onProfileLoaded(strategy, args) { | |
* redirectUrl: config.creds.redirectUrl, | ||
* allowHttpForRedirectUrl: config.creds.allowHttpForRedirectUrl, | ||
* clientSecret: config.creds.clientSecret, | ||
* thumbprint: config.creds.thumbprint, | ||
* privatePEMKey: config.crecs.privatePEMKey, | ||
* isB2C: config.creds.isB2C, | ||
* validateIssuer: config.creds.validateIssuer, | ||
* issuer: config.creds.issuer, | ||
|
@@ -445,6 +462,21 @@ function Strategy(options, verify) { | |
var validator = new Validator(validatorConfiguration); | ||
validator.validate(itemsToValidate); | ||
|
||
// validate client secret, thumbprint and privatePEMKey for hybrid and authorization flow | ||
if (options.responseType !== 'id_token') { | ||
if (options.isB2C && !options.clientSecret) { | ||
// for B2C, clientSecret is required to redeem authorization code. | ||
throw new Error('clientSecret is not provided.'); | ||
} else if (!options.clientSecret) { | ||
// for non-B2C, we can use either clientSecret or clientAssertion to redeem authorization code. | ||
// Therefore, we need either clientSecret, or privatePEMKey and thumbprint (so we can create clientAssertion). | ||
if (!options.privatePEMKey) | ||
throw new Error('privatePEMKey is not provided. Please provide either clientSecret, or privatePEMKey and thumbprint.'); | ||
if (!options.thumbprint) | ||
throw new Error('thumbprint is not provided. Please provide either clientSecret, or privatePEMKey and thumbprint.'); | ||
} | ||
} | ||
|
||
// we allow 'http' for the redirectUrl, but don't recommend using 'http' | ||
if (urlValidator.isHttpUri(options.redirectUrl)) | ||
log.warn(`Using http for redirectUrl is not recommended, please consider using https`); | ||
|
@@ -728,7 +760,7 @@ Strategy.prototype.setOptions = function setOptions(params, oauthConfig, options | |
|
||
// copy the fields needed into 'oauthConfig' | ||
aadutils.copyObjectFields(metadata.oidc, oauthConfig, ['authorization_endpoint', 'token_endpoint', 'userinfo_endpoint']); | ||
aadutils.copyObjectFields(self._options, oauthConfig, ['clientID', 'clientSecret', 'responseType', 'responseMode', 'scope', 'redirectUrl']); | ||
aadutils.copyObjectFields(self._options, oauthConfig, ['clientID', 'clientSecret', 'privatePEMKey', 'thumbprint', 'responseType', 'responseMode', 'scope', 'redirectUrl']); | ||
oauthConfig.tenantIdOrName = params.tenantIdOrName; | ||
|
||
// validate oauthConfig | ||
|
@@ -1022,29 +1054,13 @@ Strategy.prototype._authCodeFlowHandler = function authCodeFlowHandler(params, o | |
var issFromPrevIdToken = iss; | ||
var subFromPrevIdToken = sub; | ||
|
||
let libraryVersion = aadutils.getLibraryVersion(); | ||
let libraryVersionParameterName = aadutils.getLibraryVersionParameterName(); | ||
let libraryProduct = aadutils.getLibraryProduct(); | ||
let libraryProductParameterName = aadutils.getLibraryProductParameterName(); | ||
|
||
const oauth2 = new OAuth2( | ||
oauthConfig.clientID, // consumerKey | ||
oauthConfig.clientSecret, // consumer secret | ||
'', // baseURL (empty string because we use absolute urls for authorize and token paths) | ||
oauthConfig.authorization_endpoint, // authorizePath | ||
oauthConfig.token_endpoint, // accessTokenPath | ||
{libraryProductParameterName : libraryProduct, | ||
libraryVersionParameterName : libraryVersion} // customHeaders | ||
); | ||
|
||
return oauth2.getOAuthAccessToken(code, { | ||
grant_type: 'authorization_code', | ||
redirect_uri: oauthConfig.redirectUrl, | ||
scope: oauthConfig.scope | ||
}, (getOAuthAccessTokenError, access_token, refresh_token, items) => { | ||
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')); | ||
|
@@ -1093,6 +1109,7 @@ Strategy.prototype._authCodeFlowHandler = function authCodeFlowHandler(params, o | |
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) | ||
|
@@ -1211,6 +1228,61 @@ Strategy.prototype._flowInitializationHandler = function flowInitializationHandl | |
return self.redirect(location); | ||
}; | ||
|
||
/** | ||
* get access_token using client secret or client assertion | ||
* | ||
* @params {String} code | ||
* @params {Object} oauthConfig | ||
* @params {Function} callback | ||
*/ | ||
Strategy.prototype._getAccessTokenBySecretOrAssertion = (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.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: ' + post_params['client_assertion']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. recommend not logging the client assertion, just the thumbprint. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
}; | ||
|
||
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); | ||
} | ||
}); | ||
}; | ||
|
||
/** | ||
* fail and log the given message | ||
* | ||
|
@@ -1221,4 +1293,28 @@ Strategy.prototype.failWithLog = function(message) { | |
return this.fail(message); | ||
}; | ||
|
||
/** | ||
* create an oauth2 instance | ||
* | ||
* @params {Object} oauthConfig | ||
*/ | ||
var createOauth2Instance = function(oauthConfig) { | ||
let libraryVersion = aadutils.getLibraryVersion(); | ||
let libraryVersionParameterName = aadutils.getLibraryVersionParameterName(); | ||
let libraryProduct = aadutils.getLibraryProduct(); | ||
let libraryProductParameterName = aadutils.getLibraryProductParameterName(); | ||
|
||
var oauth2 = new OAuth2( | ||
oauthConfig.clientID, // consumerKey | ||
oauthConfig.clientSecret, // consumer secret | ||
'', // baseURL (empty string because we use absolute urls for authorize and token paths) | ||
oauthConfig.authorization_endpoint, // authorizePath | ||
oauthConfig.token_endpoint, // accessTokenPath | ||
{libraryProductParameterName : libraryProduct, | ||
libraryVersionParameterName : libraryVersion} // customHeaders | ||
); | ||
|
||
return oauth2; | ||
}; | ||
|
||
module.exports = Strategy; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add context in this error message indicating that the scenario is B2C.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done