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<T> { public async requestAccessToken( authorizationCode: string, sessionState: string, + state: string, userID?: string ): Promise<TokenResponse> { 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); }); } 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" 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"; diff --git a/lib/src/core/authentication-core.ts b/lib/src/core/authentication-core.ts index 8f26c537..53573969 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"; @@ -100,10 +101,13 @@ export class AuthenticationCore<T> { 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); } @@ -121,12 +125,21 @@ export class AuthenticationCore<T> { } } + 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<TokenResponse> { const tokenEndpoint = (await this._oidcProviderMetaData()).token_endpoint; @@ -161,8 +174,17 @@ export class AuthenticationCore<T> { 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, { diff --git a/lib/src/helpers/authentication-helper.ts b/lib/src/helpers/authentication-helper.ts index 1f33f70f..9a5616bb 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"; @@ -76,9 +79,9 @@ export class AuthenticationHelper<T> { 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 ] : ""; }); } @@ -92,17 +95,17 @@ export class AuthenticationHelper<T> { 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 }; @@ -120,7 +123,7 @@ export class AuthenticationHelper<T> { "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." ) ); } @@ -145,7 +148,7 @@ export class AuthenticationHelper<T> { } 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. @@ -155,7 +158,7 @@ export class AuthenticationHelper<T> { 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( @@ -218,12 +221,12 @@ export class AuthenticationHelper<T> { 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 { @@ -329,4 +332,27 @@ export class AuthenticationHelper<T> { 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<string> { + 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 }`; + } } 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"; diff --git a/lib/src/utils/authentication-utils.ts b/lib/src/utils/authentication-utils.ts index b8e6defa..3da23998 100644 --- a/lib/src/utils/authentication-utils.ts +++ b/lib/src/utils/authentication-utils.ts @@ -16,6 +16,7 @@ * under the License. */ +import { PKCE_CODE_VERIFIER, PKCE_SEPARATOR } from "../constants"; import { DecodedIDTokenPayload } from "../models"; export class AuthenticationUtils { @@ -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 }`; + } }