From dd4aae92eafbdde5ac11c2d7d422d150ceed45da Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 19 Dec 2019 23:01:50 +0100 Subject: [PATCH] feat: allow consuming JARM responses (jwt response mode) --- docs/README.md | 1 + lib/client.js | 51 +++++++++++++++++++++++++++++++++++++++++-- lib/helpers/consts.js | 22 ++++++++++--------- types/index.d.ts | 8 +++++++ 4 files changed, 70 insertions(+), 12 deletions(-) diff --git a/docs/README.md b/docs/README.md index fcfd050f..90d6507f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -283,6 +283,7 @@ Performs the callback for Authorization Server's authorization response. - `state`: `` When provided the authorization response's state parameter will be checked to be the this expected one. Use of this check is required if you sent a state parameter into an authorization request. + - `jarm`: `` When provided the authorization response must be a JARM one. - `nonce`: `` When provided the authorization response's ID Token nonce parameter will be checked to be the this expected one. Use of this check is required if you sent a nonce parameter into an authorization request. diff --git a/lib/client.js b/lib/client.js index 6398fdfb..a589f7ea 100644 --- a/lib/client.js +++ b/lib/client.js @@ -373,7 +373,18 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base checks = {}, { exchangeBody, clientAssertionPayload } = {}, ) { - const params = pickCb(parameters); + let params = pickCb(parameters); + + if (checks.jarm && !('response' in parameters)) { + throw new RPError({ + message: 'expected a JARM response', + checks, + params, + }); + } else if ('response' in parameters) { + const decrypted = await this.decryptJARM(params.response); + params = await this.validateJARM(decrypted); + } if (this.default_max_age && !checks.max_age) { checks.max_age = this.default_max_age; @@ -475,7 +486,18 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base checks = {}, { exchangeBody, clientAssertionPayload } = {}, ) { - const params = pickCb(parameters); + let params = pickCb(parameters); + + if (checks.jarm && !('response' in parameters)) { + throw new RPError({ + message: 'expected a JARM response', + checks, + params, + }); + } else if ('response' in parameters) { + const decrypted = await this.decryptJARM(params.response); + params = await this.validateJARM(decrypted); + } if (params.state && !checks.state) { throw new TypeError('checks.state argument is missing'); @@ -582,6 +604,31 @@ module.exports = (issuer, aadIssValidation = false) => class Client extends Base return this.validateJWT(body, expectedAlg, []); } + /** + * @name decryptJARM + * @api private + */ + async decryptJARM(response) { + if (!this.authorization_encrypted_response_alg && !this.authorization_encrypted_response_enc) { + return response; + } + + const expectedAlg = this.authorization_encrypted_response_alg; + const expectedEnc = this.authorization_encrypted_response_enc; + + return this.decryptJWE(response, expectedAlg, expectedEnc); + } + + /** + * @name validateJARM + * @api private + */ + async validateJARM(response) { + const expectedAlg = this.authorization_signed_response_alg; + const { payload } = await this.validateJWT(response, expectedAlg, ['iss', 'exp', 'aud']); + return pickCb(payload); + } + /** * @name decryptJWTUserinfo * @api private diff --git a/lib/helpers/consts.js b/lib/helpers/consts.js index ba4f0264..aab5b9ab 100644 --- a/lib/helpers/consts.js +++ b/lib/helpers/consts.js @@ -11,6 +11,7 @@ const AAD_MULTITENANT_DISCOVERY = new Set([ const CLIENT_DEFAULTS = { grant_types: ['authorization_code'], id_token_signed_response_alg: 'RS256', + authorization_signed_response_alg: 'RS256', response_types: ['code'], token_endpoint_auth_method: 'client_secret_basic', }; @@ -27,16 +28,17 @@ const ISSUER_DEFAULTS = { }; const CALLBACK_PROPERTIES = [ - 'access_token', - 'code', - 'error', - 'error_description', - 'error_uri', - 'expires_in', - 'id_token', - 'state', - 'token_type', - 'session_state', + 'access_token', // 6749 + 'code', // 6749 + 'error', // 6749 + 'error_description', // 6749 + 'error_uri', // 6749 + 'expires_in', // 6749 + 'id_token', // Core 1.0 + 'state', // 6749 + 'token_type', // 6749 + 'session_state', // Session Management + 'response', // JARM ]; const JWT_CONTENT = /^application\/jwt/; diff --git a/types/index.d.ts b/types/index.d.ts index 3d60171f..9190cb37 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -68,6 +68,9 @@ export interface ClientMetadata { userinfo_encrypted_response_alg?: string; userinfo_encrypted_response_enc?: string; userinfo_signed_response_alg?: string; + authorization_encrypted_response_alg?: string; + authorization_encrypted_response_enc?: string; + authorization_signed_response_alg?: string; [key: string]: unknown; } @@ -133,6 +136,7 @@ export interface CallbackParamsType { state?: string; token_type?: string; session_state?: string; + response?: string; [key: string]: unknown; } @@ -153,6 +157,10 @@ export interface OAuthCallbackChecks { * if you sent a code_challenge parameter into an authorization request. */ code_verifier?: string; + /** + * This must be set to true when requesting JARM responses. + */ + jarm?: boolean; } export interface OpenIDCallbackChecks extends OAuthCallbackChecks {