From 4025dffaa4fbd3abc309e573292f8890b84878bb Mon Sep 17 00:00:00 2001 From: Sijun Liu Date: Fri, 16 Dec 2016 16:51:19 -0800 Subject: [PATCH] Issue #231 Support client_assertion for OIDC auth flow For hybrid/code flow, client secret or client assertion is required to redeem the authorization code for access token. This fix enables user to use client assertion. 1. Added `thumbprint` and `privatePEMKey` options, so user can use the client assertion flow. However, client secret flow takes precedence. We will use client secret flow if `clientSecret` option is provided by user, regardless the values of `thumbprint` and `privatePEMKey`. 2. Added unit tests and end to end tests for client assertion flow. --- README.md | 18 ++- lib/constants.js | 3 + lib/jsonWebToken.js | 37 +++++ lib/oidcstrategy.js | 140 +++++++++++++++--- package.json | 2 +- .../b2c_oidc_hybrid_and_code_flow_test.js | 27 +++- .../oidc_hybrid_and_code_flow_test.js | 12 +- test/End_to_end_test/oidc_v1_test.js | 55 ++++++- test/End_to_end_test/oidc_v2_test.js | 29 +++- test/End_to_end_test/script.js | 25 ++++ test/End_to_end_test/test_parameters.js | 4 + test/Nodeunit_test/oidc_test.js | 2 + 12 files changed, 314 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 8746a43a..31cd46d3 100644 --- a/README.md +++ b/README.md @@ -140,9 +140,23 @@ passport.use(new OIDCStrategy({ Required to set to true if you want to use http url for redirectUrl like `http://localhost:3000`. * `clientSecret` (Conditional) + + When `responseType` is not `id_token`, we have to provide client credential to redeem the authorization code. This credential could be client secret or client assertion. Non-B2C tenant supports both flows, but B2C tenant only supports client secret flow. - 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. - + For B2C tenant: `clientSecret` is required if the `responseType` is not 'id_token'. + + For non-B2C tenant: If `responseType` is not `id_token`, developer must provide either `clientSecret`, or `thumbprint` and `privatePEMKey`. We use `clientSecret` if it is provided; otherwise we use `thumbprint` and `privatePEMKey` for client assertion flow. + + `clientSecret` 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. + +* `thumbprint` (Conditional) + + Required if you want to use client assertion flow. `thumbprint` is the base64url format of the thumbprint (hash value) of the public key. + +* `privatePEMKey` (Conditional) + + Required if you want to use client assertion flow. `privatePEMKey` is the private pem key string. + * `isB2C` (Conditional) Required to set to true if you are using B2C tenant. diff --git a/lib/constants.js b/lib/constants.js index c01c60f5..978951a6 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -33,4 +33,7 @@ CONSTANTS.TENANTID_REGEX = /^[0-9a-zA-Z-]+$/; CONSTANTS.CLOCK_SKEW = 300; // 5 minutes +CONSTANTS.CLIENT_ASSERTION_JWT_LIFETIME = 600; // 10 minutes +CONSTANTS.CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'; + module.exports = CONSTANTS; diff --git a/lib/jsonWebToken.js b/lib/jsonWebToken.js index 269ca1cc..0cd8c8c7 100644 --- a/lib/jsonWebToken.js +++ b/lib/jsonWebToken.js @@ -23,6 +23,8 @@ 'use restrict'; +const aadutils = require('./aadutils'); +const CONSTANTS = require('./constants'); const jws = require('jws'); // check if two arrays have common elements @@ -210,3 +212,38 @@ exports.verify = function(jwtString, PEMKey, options, callback) { return done(null, payload); }; + +/* Generate client assertion + * + * @params {String} clientID + * @params {String} token_endpoint + * @params {String} privatePEMKey + * @params {String} thumbprint + * @params {Function} callback + */ +exports.generateClientAssertion = function(clientID, token_endpoint, privatePEMKey, thumbprint, callback) { + var header = { 'x5t': thumbprint, 'alg': 'RS256', 'typ': 'JWT' }; + var payload = { + sub: clientID, + iss: clientID, + jti: Date.now() + aadutils.uid(16), + nbf: Math.floor(Date.now()/1000), + exp: Math.floor(Date.now()/1000) + CONSTANTS.CLIENT_ASSERTION_JWT_LIFETIME, + aud: token_endpoint, + }; + + var clientAssertion; + + try { + clientAssertion = jws.sign({ + header: header, + payload: payload, + privateKey: privatePEMKey + }); + } catch (ex) { + callback(ex); + } + + callback(null, clientAssertion); +}; + diff --git a/lib/oidcstrategy.js b/lib/oidcstrategy.js index 2aafd863..667ed765 100644 --- a/lib/oidcstrategy.js +++ b/lib/oidcstrategy.js @@ -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']); + }; + + 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; diff --git a/package.json b/package.json index 8f87fda8..1a1c5124 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "cache-manager": "^2.0.0", "jws": "^3.1.3", "lodash": "^4.11.2", - "oauth": "^0.9.14", + "oauth": "0.9.14", "passport": "^0.3.2", "pkginfo": "^0.4.0", "request": "^2.72.0", diff --git a/test/Chai-passport_test/b2c_oidc_hybrid_and_code_flow_test.js b/test/Chai-passport_test/b2c_oidc_hybrid_and_code_flow_test.js index a8412802..c74a7a6b 100644 --- a/test/Chai-passport_test/b2c_oidc_hybrid_and_code_flow_test.js +++ b/test/Chai-passport_test/b2c_oidc_hybrid_and_code_flow_test.js @@ -58,6 +58,7 @@ wfp+cqZbCd9TENyHaTb8iA27s+73L3ExOQIDAQAB\n\ var options = { redirectUrl: 'https://localhost:3000/auth/openid/return', clientID: 'f0b6e4eb-2d8c-40b6-b9c6-e26d1074846d', + clientSecret: 'secret', identityMetadata: 'https://login.microsoftonline.com/mytenant.onmicrosoft.com/v2.0/.well-known/openid-configuration', responseType: 'code id_token', responseMode: 'form_post', @@ -80,20 +81,30 @@ var testStrategy = new OIDCStrategy(options, function(profile, done) { // mock the token response we want when we consume the code var setTokenResponse = function(id_token_in_token_resp, access_token_in_token_resp) { return () => { - OAuth2.prototype.getOAuthAccessToken = function(code, params, callback) { - params = {'id_token': id_token_in_token_resp, 'token_type': 'Bearer'}; - callback(null, access_token_in_token_resp, null, params); - } + testStrategy._getAccessTokenBySecretOrAssertion = function(code, oauthConfig, next, callback) { + var params = { + 'id_token': id_token_in_token_resp, + 'token_type': 'Bearer', + 'access_token': access_token_in_token_resp, + 'refresh_token': null + }; + callback(null, params); + }; }; }; // mock the token response we want when we consume the code, and use 'bearer' fpr token_type var setTokenResponseWithLowerCaseBearer = function(id_token_in_token_resp, access_token_in_token_resp) { return () => { - OAuth2.prototype.getOAuthAccessToken = function(code, params, callback) { - params = {'id_token': id_token_in_token_resp, 'token_type': 'bearer'}; - callback(null, access_token_in_token_resp, null, params); - } + testStrategy._getAccessTokenBySecretOrAssertion = function(code, oauthConfig, next, callback) { + var params = { + 'id_token': id_token_in_token_resp, + 'token_type': 'Bearer', + 'access_token': access_token_in_token_resp, + 'refresh_token': null + }; + callback(null, params); + }; }; }; diff --git a/test/Chai-passport_test/oidc_hybrid_and_code_flow_test.js b/test/Chai-passport_test/oidc_hybrid_and_code_flow_test.js index d4317add..aeb38717 100644 --- a/test/Chai-passport_test/oidc_hybrid_and_code_flow_test.js +++ b/test/Chai-passport_test/oidc_hybrid_and_code_flow_test.js @@ -61,6 +61,7 @@ KzveKf3l5UU3c6PkGy+BB3E/ChqFm6sPWwIDAQAB\n\ var options = { redirectUrl: 'https://localhost:3000/auth/openid/return', clientID: '2abf3a52-7d86-460b-a1ef-77dc43de8aad', + clientSecret: 'secret', identityMetadata: 'https://login.microsoftonline.com/sijun.onmicrosoft.com/.well-known/openid-configuration', responseType: 'id_token code', responseMode: 'form_post', @@ -105,9 +106,14 @@ var setUserInfoResponse = function(sub_choice) { // mock the token response we want when we consume the code var setTokenResponse = function(id_token_in_token_resp, access_token_in_token_resp) { return () => { - OAuth2.prototype.getOAuthAccessToken = function(code, params, callback) { - params = {'id_token': id_token_in_token_resp, 'token_type': 'Bearer'}; - callback(null, access_token_in_token_resp, refresh_token, params); + testStrategy._getAccessTokenBySecretOrAssertion = function(code, oauthConfig, next, callback) { + var params = { + 'id_token': id_token_in_token_resp, + 'token_type': 'Bearer', + 'access_token': access_token_in_token_resp, + 'refresh_token': refresh_token + }; + callback(null, params); } }; }; diff --git a/test/End_to_end_test/oidc_v1_test.js b/test/End_to_end_test/oidc_v1_test.js index 887e4339..e45ab77e 100644 --- a/test/End_to_end_test/oidc_v1_test.js +++ b/test/End_to_end_test/oidc_v1_test.js @@ -50,7 +50,8 @@ var test_parameters = {}; // tenant specific endpoint configurations var config_template, hybrid_config, hybrid_config_alternative, code_config, implicit_config, code_config_query, hybrid_config_noIssuer, hybrid_config_with_scope, -hybrid_config_passReqToCallback = {}; +hybrid_config_passReqToCallback, +hybrid_config_clientAssertion, code_config_clientAssertion = {}; // common endpoint configurations var config_template_common_endpoint, hybrid_config_common_endpoint, @@ -61,7 +62,9 @@ hybrid_config_common_endpoint_with_scope = {}; // invalid configurations var hybrid_config_common_endpoint_wrong_issuer, hybrid_config_common_endpoint_short_lifetime, -hybrid_config_common_endpoint_wrong_secret = {}; +hybrid_config_common_endpoint_wrong_secret, +hybrid_config_clientAssertion_invalid_pemKey, +hybrid_config_clientAssertion_wrong_thumbprint = {}; // drivers needed for the tests var driver; @@ -99,7 +102,7 @@ var apply_test_parameters = (done) => { responseMode: 'form_post', redirectUrl: 'http://localhost:3000/auth/openid/return', allowHttpForRedirectUrl: true, - clientSecret: test_parameters.clientSecret, + clientSecret: test_parameters.clientSecret, validateIssuer: true, issuer: ['https://sts.windows.net/' + test_parameters.tenantID + '/'], passReqToCallback: false, @@ -141,6 +144,18 @@ var apply_test_parameters = (done) => { hybrid_config_passReqToCallback = JSON.parse(JSON.stringify(config_template)); hybrid_config_passReqToCallback.passReqToCallback = true; + // 6. Hybird flow using client assertion + hybrid_config_clientAssertion = JSON.parse(JSON.stringify(hybrid_config)); + hybrid_config_clientAssertion.thumbprint = test_parameters.thumbprint; + hybrid_config_clientAssertion.privatePEMKey = test_parameters.privatePEMKey; + hybrid_config_clientAssertion.clientSecret = null; + + // 7. Code flow using client assertion + code_config_clientAssertion = JSON.parse(JSON.stringify(code_config)); + code_config_clientAssertion.thumbprint = test_parameters.thumbprint; + code_config_clientAssertion.privatePEMKey = test_parameters.privatePEMKey; + code_config_clientAssertion.clientSecret = null; + /**************************************************************************** * Tenant specific endpoint configurations ***************************************************************************/ @@ -186,6 +201,16 @@ var apply_test_parameters = (done) => { // 2. common endpoint with wrong client secret hybrid_config_common_endpoint_wrong_secret = JSON.parse(JSON.stringify(config_template_common_endpoint)); hybrid_config_common_endpoint_wrong_secret.clientSecret = 'wrong_secret'; + // 3. Hybird flow using client assertion with invalid privatePEMKey + hybrid_config_clientAssertion_invalid_pemKey = JSON.parse(JSON.stringify(hybrid_config)); + hybrid_config_clientAssertion_invalid_pemKey.thumbprint = test_parameters.thumbprint; + hybrid_config_clientAssertion_invalid_pemKey.privatePEMKey = 'invalid private pem key'; + hybrid_config_clientAssertion_invalid_pemKey.clientSecret = null; + // 4. hybrid flow using client assertion with wrong thumbprint + hybrid_config_clientAssertion_wrong_thumbprint = JSON.parse(JSON.stringify(hybrid_config)); + hybrid_config_clientAssertion_wrong_thumbprint.thumbprint = 'wrongThumbprint'; + hybrid_config_clientAssertion_wrong_thumbprint.privatePEMKey = test_parameters.privatePEMKey; + hybrid_config_clientAssertion_wrong_thumbprint.clientSecret = null; done(); }; @@ -474,6 +499,20 @@ describe('oidc v1 positive other test', function() { checkResult(implicit_config, 2, done); }); + /**************************************************************************** + * Test client assertion + ***************************************************************************/ + + // hybrid flow using client assertion + it('should succeed', function(done) { + checkResult(hybrid_config_clientAssertion, 8, done); + }); + + // code flow using client assertion + it('should succeed', function(done) { + checkResult(code_config_clientAssertion, 8, done); + }); + /**************************************************************************** * Test various response type for common endpoint ***************************************************************************/ @@ -582,6 +621,16 @@ describe('oidc v1 negative test', function() { checkInvalidResult(hybrid_config_common_endpoint_wrong_secret, done); }); + // invalid privatePEMKey + it('should fail with invalid privatePEMKey', function(done) { + checkInvalidResult(hybrid_config_clientAssertion_invalid_pemKey, done); + }); + + // wrong thumbprint + it('should fail with wrong thumbprint', function(done) { + checkInvalidResult(hybrid_config_clientAssertion_wrong_thumbprint, done); + }); + it('close service', function(done) { expect('1').to.equal('1'); driver.quit(); diff --git a/test/End_to_end_test/oidc_v2_test.js b/test/End_to_end_test/oidc_v2_test.js index 6e7f057b..71316a05 100644 --- a/test/End_to_end_test/oidc_v2_test.js +++ b/test/End_to_end_test/oidc_v2_test.js @@ -50,7 +50,8 @@ var test_parameters = {}; // tenant specific endpoint configurations var config_template, hybrid_config, hybrid_config_alternative, code_config, implicit_config, hybrid_config_passReqToCallback, code_config_query, -hybrid_config_noIssuer, hybrid_config_with_scope = {}; +hybrid_config_noIssuer, hybrid_config_with_scope, +hybrid_config_clientAssertion, code_config_clientAssertion = {}; // common endpoint configurations var config_template_common_endpoint, hybrid_config_common_endpoint, @@ -144,6 +145,18 @@ var apply_test_parameters = (done) => { hybrid_config_with_scope = JSON.parse(JSON.stringify(config_template)); hybrid_config_with_scope.scope = ['email', 'profile', 'offline_access', 'https://graph.microsoft.com/mail.read']; + // 6. Hybird flow using client assertion + hybrid_config_clientAssertion = JSON.parse(JSON.stringify(hybrid_config)); + hybrid_config_clientAssertion.thumbprint = test_parameters.thumbprint; + hybrid_config_clientAssertion.privatePEMKey = test_parameters.privatePEMKey; + hybrid_config_clientAssertion.clientSecret = null; + + // 7. Code flow using client assertion + code_config_clientAssertion = JSON.parse(JSON.stringify(code_config)); + code_config_clientAssertion.thumbprint = test_parameters.thumbprint; + code_config_clientAssertion.privatePEMKey = test_parameters.privatePEMKey; + code_config_clientAssertion.clientSecret = null; + /****************************************************************************** * Common endpoint configurations *****************************************************************************/ @@ -346,6 +359,20 @@ describe('oidc v2 positive test', function() { checkResult(implicit_config, done); }); + /**************************************************************************** + * Test client assertion + ***************************************************************************/ + + // hybrid flow using client assertion + it('should succeed', function(done) { + checkResult(hybrid_config_clientAssertion, done); + }); + + // code flow using client assertion + it('should succeed', function(done) { + checkResult(code_config_clientAssertion, done); + }); + /*************************************************************************** * Test various response type for common endpoint **************************************************************************/ diff --git a/test/End_to_end_test/script.js b/test/End_to_end_test/script.js index 03907efd..a037a690 100644 --- a/test/End_to_end_test/script.js +++ b/test/End_to_end_test/script.js @@ -32,6 +32,7 @@ var async = require('async'); var clientId = process.env.KEY_VAULT_CLIENT_ID; var clientSecret = process.env.KEY_VAULT_CLIENT_SECRET; + var authenticator = (challenge, callback) => { // Create a new authentication context. var context = new adalNode.AuthenticationContext(challenge.authorization); @@ -57,8 +58,12 @@ var b2c_kv_name = 'PassportB2C-lsj31415926atgmaildotcom'; var b2c_password; var v1_client_secret_kv_name = 'PassportV1ClientSecret'; var v1_client_secret; +var v1_private_pem_key_kv_name = 'PassportV1PrivatePEMKey'; +var v1_private_pem_key; var v2_client_secret_kv_name = 'PassportV2ClientSecret'; var v2_client_secret; +var v2_private_pem_key_kv_name = 'PassportV2PrivatePEMKey'; +var v2_private_pem_key; var b2c_client_secret_kv_name = 'PassportB2CClientSecret'; var b2c_client_secret; @@ -98,6 +103,22 @@ exports.set_test_parameters = (callback) => { }); }, + (next) => { + client.getSecret(vaultUri + '/secrets/' + v1_private_pem_key_kv_name, function (err, result) { + if (err) throw err; + v1_private_pem_key = result.value.replace(/\\n/g, '\n'); + return next(); + }); + }, + + (next) => { + client.getSecret(vaultUri + '/secrets/' + v2_private_pem_key_kv_name, function (err, result) { + if (err) throw err; + v2_private_pem_key = result.value.replace(/\\n/g, '\n'); + return next(); + }); + }, + (next) => { client.getSecret(vaultUri + '/secrets/' + b2c_client_secret_kv_name, function (err, result) { if (err) throw err; @@ -111,6 +132,8 @@ exports.set_test_parameters = (callback) => { tenantID: 'd34a1bb7-3481-4d5f-8b94-f3cc27bf8eac', clientID: '53d378fd-9c04-4e99-bb8e-6c9f144fe440', clientSecret: v1_client_secret, + thumbprint: 'Z2mGlF+IHL49Q9a66mDQLWG/lfs=', + privatePEMKey: v1_private_pem_key, username: 'manNonMFA1@msidlab5.onmicrosoft.com', password: v1_v2_password, oid: 'a9590750-7562-45f5-af5b-41f504843766' @@ -120,6 +143,8 @@ exports.set_test_parameters = (callback) => { tenantID: 'd34a1bb7-3481-4d5f-8b94-f3cc27bf8eac', clientID: 'fa300e1e-ba08-4717-b97f-af77a9f65199', clientSecret: v2_client_secret, + thumbprint: 'IjDSnLDGTTqGCFOgfMURlYtKMro', + privatePEMKey: v2_private_pem_key, username: 'manNonMFA1@msidlab5.onmicrosoft.com', password: v1_v2_password, oid: 'a9590750-7562-45f5-af5b-41f504843766' diff --git a/test/End_to_end_test/test_parameters.js b/test/End_to_end_test/test_parameters.js index fa3d1894..a8b6fa4c 100644 --- a/test/End_to_end_test/test_parameters.js +++ b/test/End_to_end_test/test_parameters.js @@ -28,6 +28,8 @@ var test_parameters = { tenantID: '', clientID: '', clientSecret: '', + thumbprint: '', + privatePEMKey: '', username: '', password: '', oid: '', @@ -36,6 +38,8 @@ var test_parameters = { tenantID: '', clientID: '', clientSecret: '', + thumbprint: '', + privatePEMKey: '', username: '', password: '', oid: '', diff --git a/test/Nodeunit_test/oidc_test.js b/test/Nodeunit_test/oidc_test.js index 15fa6e46..acf46775 100644 --- a/test/Nodeunit_test/oidc_test.js +++ b/test/Nodeunit_test/oidc_test.js @@ -57,6 +57,7 @@ function setConfig(redirectUrl, clientID, responseType, responseMode, validateIs responseType: responseType, responseMode: responseMode, validateIssuer: validateIssuer, + clientSecret: 'secret' }; testCallback(config); @@ -71,6 +72,7 @@ function setConfigCommon(redirectUrl, clientID, responseType, responseMode, vali responseMode: responseMode, validateIssuer: validateIssuer, issuer: issuer, + clientSecret: 'secret' }; testCallback(config);