From e07845a7a174aab37f03c549e612c26638c3148e Mon Sep 17 00:00:00 2001 From: Theviyanthan Date: Mon, 28 Feb 2022 23:13:14 +0530 Subject: [PATCH 1/8] Create a method to generate PKCE keys --- lib/src/helpers/authentication-helper.ts | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/src/helpers/authentication-helper.ts b/lib/src/helpers/authentication-helper.ts index 1f33f70f..e66489c5 100644 --- a/lib/src/helpers/authentication-helper.ts +++ b/lib/src/helpers/authentication-helper.ts @@ -26,6 +26,8 @@ import { JWKS_ENDPOINT, OIDC_SCOPE, OIDC_SESSION_IFRAME_ENDPOINT, + PKCE_CODE_VERIFIER, + PKCE_SEPARATOR, REVOKE_TOKEN_ENDPOINT, SCOPE_TAG, SERVICE_RESOURCES, @@ -43,6 +45,7 @@ import { FetchResponse, OIDCEndpointsInternal, OIDCProviderMetaData, + TemporaryData, TokenResponse } from "../models"; import { AuthenticationUtils } from "../utils"; @@ -329,4 +332,28 @@ export class AuthenticationHelper { return Promise.resolve(tokenResponse); } } + + /** + * This generates a PKCE key with the right index value. + * + * @param {string} userID The userID to identify a user in a multi-user scenario. + * + * @returns {string} The PKCE key. + */ + public async generatePKCEKey(userID?: string): Promise { + const tempData: TemporaryData = await this._dataLayer.getTemporaryData(userID); + const keys: string[] = []; + + Object.keys(tempData).forEach((key: string) => { + if (key.startsWith(PKCE_CODE_VERIFIER)) { + keys.push(key); + } + }); + + const lastKey: string | undefined = keys.sort().pop(); + + const index: number = parseInt(lastKey?.split(PKCE_SEPARATOR)[ 1 ] ?? "-1"); + + return `${ PKCE_CODE_VERIFIER }${ PKCE_SEPARATOR }${ index + 1 }`; + } } From 0d121f1221e752c55eb94f99b511c3abb8bdbfd6 Mon Sep 17 00:00:00 2001 From: Theviyanthan Date: Mon, 28 Feb 2022 23:13:48 +0530 Subject: [PATCH 2/8] Add util functions to extract PKCE key from state param and to generate state param --- lib/src/utils/authentication-utils.ts | 31 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/src/utils/authentication-utils.ts b/lib/src/utils/authentication-utils.ts index b8e6defa..7ee17726 100644 --- a/lib/src/utils/authentication-utils.ts +++ b/lib/src/utils/authentication-utils.ts @@ -16,11 +16,12 @@ * under the License. */ +import { PKCE_CODE_VERIFIER, PKCE_SEPARATOR } from "../constants"; import { DecodedIDTokenPayload } from "../models"; export class AuthenticationUtils { // eslint-disable-next-line @typescript-eslint/no-empty-function - private constructor() { } + private constructor() {} public static filterClaimsFromIDTokenPayload(payload: DecodedIDTokenPayload): any { const optionalizedPayload: Partial = { ...payload }; @@ -41,7 +42,7 @@ export class AuthenticationUtils { delete optionalizedPayload?.sid; const camelCasedPayload = {}; - Object.entries(optionalizedPayload).forEach(([ key, value ]) => { + Object.entries(optionalizedPayload).forEach(([key, value]) => { const keyParts = key.split("_"); const camelCasedKey = keyParts .map((key: string, index: number) => { @@ -49,11 +50,11 @@ export class AuthenticationUtils { return key; } - return [ key[ 0 ].toUpperCase(), ...key.slice(1) ].join(""); + return [key[0].toUpperCase(), ...key.slice(1)].join(""); }) .join(""); - camelCasedPayload[ camelCasedKey ] = value; + camelCasedPayload[camelCasedKey] = value; }); return camelCasedPayload; @@ -69,7 +70,7 @@ export class AuthenticationUtils { // This works only when the email is used as the username // and the tenant domain is appended to the`sub` attribute. - return tokens.length > 2 ? tokens[ tokens.length - 1 ] : ""; + return tokens.length > 2 ? tokens[tokens.length - 1] : ""; }; public static getTokenRequestHeaders(): HeadersInit { @@ -78,4 +79,24 @@ export class AuthenticationUtils { "Content-Type": "application/x-www-form-urlencoded" }; } + + /** + * This generates the state param value to be sent with an authorization request. + * + * @param {string} pkceKey The PKCE key. + * @param {string} state The state value to be passed. (The correlation ID will be appended to this state value.) + * + * @returns {string} The state param value. + */ + public static generateStateParamForRequestCorrelation(pkceKey: string, state?: string): string { + const index: number = parseInt(pkceKey.split(PKCE_SEPARATOR)[ 1 ]); + + return state ? `${ state }_request_${ index }` : `request_${ index }`; + } + + public static extractPKCEKeyFromStateParam(stateParam: string): string { + const index: number = parseInt(stateParam.split("request_")[ 1 ]); + + return `${ PKCE_CODE_VERIFIER }${PKCE_SEPARATOR}${ index }`; + } } From 2881974afb0c6d1b41fd1e5d8697a3a807faf990 Mon Sep 17 00:00:00 2001 From: Theviyanthan Date: Mon, 28 Feb 2022 23:15:21 +0530 Subject: [PATCH 3/8] Use the helper and util functions to generate pkce keys and state params --- lib/src/core/authentication-core.ts | 31 +++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/src/core/authentication-core.ts b/lib/src/core/authentication-core.ts index 8f26c537..184bb5e1 100644 --- a/lib/src/core/authentication-core.ts +++ b/lib/src/core/authentication-core.ts @@ -22,7 +22,8 @@ import { OP_CONFIG_INITIATED, PKCE_CODE_VERIFIER, SESSION_STATE, - SIGN_OUT_SUCCESS_PARAM + SIGN_OUT_SUCCESS_PARAM, + STATE } from "../constants"; import { DataLayer } from "../data"; import { AsgardeoAuthException, AsgardeoAuthNetworkException } from "../exception"; @@ -75,7 +76,7 @@ export class AuthenticationCore { "getAuthorizationURL", "No authorization endpoint found.", "No authorization endpoint was found in the OIDC provider meta data from the well-known endpoint " + - "or the authorization endpoint passed to the SDK is empty." + "or the authorization endpoint passed to the SDK is empty." ); } @@ -100,10 +101,13 @@ export class AuthenticationCore { authorizeRequest.searchParams.append("response_mode", configData.responseMode); } + const pkceKey: string = await this._authenticationHelper.generatePKCEKey(userID); + if (configData.enablePKCE) { const codeVerifier = this._cryptoHelper?.getCodeVerifier(); const codeChallenge = this._cryptoHelper?.getCodeChallenge(codeVerifier); - await this._dataLayer.setTemporaryDataParameter(PKCE_CODE_VERIFIER, codeVerifier, userID); + + await this._dataLayer.setTemporaryDataParameter(pkceKey, codeVerifier, userID); authorizeRequest.searchParams.append("code_challenge_method", "S256"); authorizeRequest.searchParams.append("code_challenge", codeChallenge); } @@ -114,19 +118,29 @@ export class AuthenticationCore { const customParams = config; if (customParams) { - for (const [ key, value ] of Object.entries(customParams)) { + for (const [key, value] of Object.entries(customParams)) { if (key != "" && value != "") { authorizeRequest.searchParams.append(key, value.toString()); } } } + authorizeRequest.searchParams.append( + STATE, + AuthenticationUtils.generateStateParamForRequestCorrelation( + pkceKey, + authorizeRequest.searchParams.get(STATE) ?? "" + ) + ); + + return authorizeRequest.toString(); } public async requestAccessToken( authorizationCode: string, sessionState: string, + state: string, userID?: string ): Promise { const tokenEndpoint = (await this._oidcProviderMetaData()).token_endpoint; @@ -161,8 +175,13 @@ export class AuthenticationCore { body.push(`redirect_uri=${ configData.signInRedirectURL }`); if (configData.enablePKCE) { - body.push(`code_verifier=${ await this._dataLayer.getTemporaryDataParameter(PKCE_CODE_VERIFIER, userID) }`); - await this._dataLayer.removeTemporaryDataParameter(PKCE_CODE_VERIFIER, userID); + body.push(`code_verifier=${ await this._dataLayer.getTemporaryDataParameter( + AuthenticationUtils.extractPKCEKeyFromStateParam(state), userID) }`); + + await this._dataLayer.removeTemporaryDataParameter( + AuthenticationUtils.extractPKCEKeyFromStateParam(state), + userID + ); } return fetch(tokenEndpoint, { From a5950a4bd81c4b12939a7367c0d06a5a3da7eee5 Mon Sep 17 00:00:00 2001 From: Theviyanthan Date: Mon, 28 Feb 2022 23:15:40 +0530 Subject: [PATCH 4/8] Update client --- lib/src/client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/client.ts b/lib/src/client.ts index 535f144d..96434055 100644 --- a/lib/src/client.ts +++ b/lib/src/client.ts @@ -197,14 +197,15 @@ export class AsgardeoAuthClient { public async requestAccessToken( authorizationCode: string, sessionState: string, + state: string, userID?: string ): Promise { if (await this._dataLayer.getTemporaryDataParameter(OP_CONFIG_INITIATED)) { - return this._authenticationCore.requestAccessToken(authorizationCode, sessionState, userID); + return this._authenticationCore.requestAccessToken(authorizationCode, sessionState, state, userID); } return this._authenticationCore.getOIDCProviderMetaData(false).then(() => { - return this._authenticationCore.requestAccessToken(authorizationCode, sessionState, userID); + return this._authenticationCore.requestAccessToken(authorizationCode, sessionState, state,userID); }); } From 3f5b1cf8f802a0de4ff3227264a52c0a1130765c Mon Sep 17 00:00:00 2001 From: Theviyanthan Date: Mon, 28 Feb 2022 23:15:51 +0530 Subject: [PATCH 5/8] Create PKCE separator constant --- lib/src/constants/data.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/constants/data.ts b/lib/src/constants/data.ts index 04214175..1cb590b3 100644 --- a/lib/src/constants/data.ts +++ b/lib/src/constants/data.ts @@ -25,6 +25,7 @@ export enum Stores { export const REFRESH_TOKEN_TIMER = "refresh_token_timer"; export const PKCE_CODE_VERIFIER = "pkce_code_verifier"; +export const PKCE_SEPARATOR = "#"; export const SUPPORTED_SIGNATURE_ALGORITHMS = [ "RS256", "RS512", "RS384", "PS256" From 0cf0d12f0e1c6acd04feb05c93a43b6954a57588 Mon Sep 17 00:00:00 2001 From: Theviyanthan Date: Mon, 28 Feb 2022 23:17:21 +0530 Subject: [PATCH 6/8] Add state param constant --- lib/src/constants/parameters.ts | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/src/constants/parameters.ts b/lib/src/constants/parameters.ts index fca25c3b..f3d649d5 100644 --- a/lib/src/constants/parameters.ts +++ b/lib/src/constants/parameters.ts @@ -1,22 +1,23 @@ /** -* Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. -* -* WSO2 Inc. licenses this file to you under the Apache License, -* Version 2.0 (the "License"); you may not use this file except -* in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ + * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ export const AUTHORIZATION_CODE = "code"; export const SESSION_STATE = "session_state"; export const SIGN_OUT_URL = "sign_out_url"; export const SIGN_OUT_SUCCESS_PARAM = "sign_out_success"; +export const STATE = "state"; From 03fc77d12da2aeb32185698ad3967e3b80f32e1d Mon Sep 17 00:00:00 2001 From: Theviyanthan Date: Mon, 28 Feb 2022 23:17:29 +0530 Subject: [PATCH 7/8] Expose utils --- lib/src/public-api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/public-api.ts b/lib/src/public-api.ts index 1abd6e14..9c220332 100644 --- a/lib/src/public-api.ts +++ b/lib/src/public-api.ts @@ -24,3 +24,4 @@ export * from "./constants/parameters"; export * from "./constants/data"; export * from "./constants/parameters"; export * from "./constants/scopes"; +export * from "./utils"; From 511d5985501c7ee8679027ebb2eb4fc7c73104a5 Mon Sep 17 00:00:00 2001 From: Theviyanthan Date: Mon, 28 Feb 2022 23:22:49 +0530 Subject: [PATCH 8/8] Format code --- lib/src/core/authentication-core.ts | 13 +++++--- lib/src/helpers/authentication-helper.ts | 39 ++++++++++++------------ lib/src/utils/authentication-utils.ts | 12 ++++---- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/lib/src/core/authentication-core.ts b/lib/src/core/authentication-core.ts index 184bb5e1..53573969 100644 --- a/lib/src/core/authentication-core.ts +++ b/lib/src/core/authentication-core.ts @@ -76,7 +76,7 @@ export class AuthenticationCore { "getAuthorizationURL", "No authorization endpoint found.", "No authorization endpoint was found in the OIDC provider meta data from the well-known endpoint " + - "or the authorization endpoint passed to the SDK is empty." + "or the authorization endpoint passed to the SDK is empty." ); } @@ -118,7 +118,7 @@ export class AuthenticationCore { const customParams = config; if (customParams) { - for (const [key, value] of Object.entries(customParams)) { + for (const [ key, value ] of Object.entries(customParams)) { if (key != "" && value != "") { authorizeRequest.searchParams.append(key, value.toString()); } @@ -133,7 +133,6 @@ export class AuthenticationCore { ) ); - return authorizeRequest.toString(); } @@ -175,8 +174,12 @@ export class AuthenticationCore { body.push(`redirect_uri=${ configData.signInRedirectURL }`); if (configData.enablePKCE) { - body.push(`code_verifier=${ await this._dataLayer.getTemporaryDataParameter( - AuthenticationUtils.extractPKCEKeyFromStateParam(state), userID) }`); + body.push( + `code_verifier=${ await this._dataLayer.getTemporaryDataParameter( + AuthenticationUtils.extractPKCEKeyFromStateParam(state), + userID + ) }` + ); await this._dataLayer.removeTemporaryDataParameter( AuthenticationUtils.extractPKCEKeyFromStateParam(state), diff --git a/lib/src/helpers/authentication-helper.ts b/lib/src/helpers/authentication-helper.ts index e66489c5..9a5616bb 100644 --- a/lib/src/helpers/authentication-helper.ts +++ b/lib/src/helpers/authentication-helper.ts @@ -79,9 +79,9 @@ export class AuthenticationHelper { if (configData.overrideWellEndpointConfig) { configData.endpoints && Object.keys(configData.endpoints).forEach((endpointName: string) => { - const snakeCasedName = endpointName.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); - oidcProviderMetaData[snakeCasedName] = configData?.endpoints - ? configData.endpoints[endpointName] + const snakeCasedName = endpointName.replace(/[A-Z]/g, (letter) => `_${ letter.toLowerCase() }`); + oidcProviderMetaData[ snakeCasedName ] = configData?.endpoints + ? configData.endpoints[ endpointName ] : ""; }); } @@ -95,17 +95,17 @@ export class AuthenticationHelper { configData.endpoints && Object.keys(configData.endpoints).forEach((endpointName: string) => { - const snakeCasedName = endpointName.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); - oidcProviderMetaData[snakeCasedName] = configData?.endpoints ? configData.endpoints[endpointName] : ""; + const snakeCasedName = endpointName.replace(/[A-Z]/g, (letter) => `_${ letter.toLowerCase() }`); + oidcProviderMetaData[ snakeCasedName ] = configData?.endpoints ? configData.endpoints[ endpointName ] : ""; }); const defaultEndpoints = { - [AUTHORIZATION_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.authorizationEndpoint, - [END_SESSION_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.endSessionEndpoint, - [JWKS_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.jwksUri, - [OIDC_SESSION_IFRAME_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.checkSessionIframe, - [REVOKE_TOKEN_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.revocationEndpoint, - [TOKEN_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.tokenEndpoint + [ AUTHORIZATION_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.authorizationEndpoint, + [ END_SESSION_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.endSessionEndpoint, + [ JWKS_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.jwksUri, + [ OIDC_SESSION_IFRAME_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.checkSessionIframe, + [ REVOKE_TOKEN_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.revocationEndpoint, + [ TOKEN_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.tokenEndpoint }; return { ...oidcProviderMetaData, ...defaultEndpoints }; @@ -123,7 +123,7 @@ export class AuthenticationHelper { "validateIdToken", "JWKS endpoint not found.", "No JWKS endpoint was found in the OIDC provider meta data returned by the well-known endpoint " + - "or the JWKS endpoint passed to the SDK is empty." + "or the JWKS endpoint passed to the SDK is empty." ) ); } @@ -148,7 +148,7 @@ export class AuthenticationHelper { } const issuer = (await this._oidcProviderMetaData()).issuer; - const issuerFromURL = (await this.resolveWellKnownEndpoint()).split("/.well-known")[0]; + const issuerFromURL = (await this.resolveWellKnownEndpoint()).split("/.well-known")[ 0 ]; // Return false if the issuer in the open id config doesn't match // the issuer in the well known endpoint URL. @@ -158,7 +158,7 @@ export class AuthenticationHelper { const parsedResponse = await response.json(); return this._cryptoHelper - .getJWKForTheIdToken(idToken.split(".")[0], parsedResponse.keys) + .getJWKForTheIdToken(idToken.split(".")[ 0 ], parsedResponse.keys) .then(async (jwk: any) => { return this._cryptoHelper .isValidIdToken( @@ -221,12 +221,12 @@ export class AuthenticationHelper { const familyName: string = payload.family_name ?? ""; const fullName: string = givenName && familyName - ? `${givenName} ${familyName}` + ? `${ givenName } ${ familyName }` : givenName - ? givenName - : familyName - ? familyName - : ""; + ? givenName + : familyName + ? familyName + : ""; const displayName: string = payload.preferred_username ?? fullName; return { @@ -351,7 +351,6 @@ export class AuthenticationHelper { }); const lastKey: string | undefined = keys.sort().pop(); - const index: number = parseInt(lastKey?.split(PKCE_SEPARATOR)[ 1 ] ?? "-1"); return `${ PKCE_CODE_VERIFIER }${ PKCE_SEPARATOR }${ index + 1 }`; diff --git a/lib/src/utils/authentication-utils.ts b/lib/src/utils/authentication-utils.ts index 7ee17726..3da23998 100644 --- a/lib/src/utils/authentication-utils.ts +++ b/lib/src/utils/authentication-utils.ts @@ -21,7 +21,7 @@ import { DecodedIDTokenPayload } from "../models"; export class AuthenticationUtils { // eslint-disable-next-line @typescript-eslint/no-empty-function - private constructor() {} + private constructor() { } public static filterClaimsFromIDTokenPayload(payload: DecodedIDTokenPayload): any { const optionalizedPayload: Partial = { ...payload }; @@ -42,7 +42,7 @@ export class AuthenticationUtils { delete optionalizedPayload?.sid; const camelCasedPayload = {}; - Object.entries(optionalizedPayload).forEach(([key, value]) => { + Object.entries(optionalizedPayload).forEach(([ key, value ]) => { const keyParts = key.split("_"); const camelCasedKey = keyParts .map((key: string, index: number) => { @@ -50,11 +50,11 @@ export class AuthenticationUtils { return key; } - return [key[0].toUpperCase(), ...key.slice(1)].join(""); + return [ key[ 0 ].toUpperCase(), ...key.slice(1) ].join(""); }) .join(""); - camelCasedPayload[camelCasedKey] = value; + camelCasedPayload[ camelCasedKey ] = value; }); return camelCasedPayload; @@ -70,7 +70,7 @@ export class AuthenticationUtils { // This works only when the email is used as the username // and the tenant domain is appended to the`sub` attribute. - return tokens.length > 2 ? tokens[tokens.length - 1] : ""; + return tokens.length > 2 ? tokens[ tokens.length - 1 ] : ""; }; public static getTokenRequestHeaders(): HeadersInit { @@ -97,6 +97,6 @@ export class AuthenticationUtils { public static extractPKCEKeyFromStateParam(stateParam: string): string { const index: number = parseInt(stateParam.split("request_")[ 1 ]); - return `${ PKCE_CODE_VERIFIER }${PKCE_SEPARATOR}${ index }`; + return `${ PKCE_CODE_VERIFIER }${ PKCE_SEPARATOR }${ index }`; } }