Skip to content

Commit

Permalink
Merge pull request #179 from thivi/master
Browse files Browse the repository at this point in the history
Correlate authorization request with access token request using the state parameter.
  • Loading branch information
thivi authored Feb 28, 2022
2 parents a9db4f0 + 511d598 commit 07190e8
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 41 deletions.
5 changes: 3 additions & 2 deletions lib/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}

Expand Down
1 change: 1 addition & 0 deletions lib/src/constants/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 17 additions & 16 deletions lib/src/constants/parameters.ts
Original file line number Diff line number Diff line change
@@ -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";
30 changes: 26 additions & 4 deletions lib/src/core/authentication-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
Expand Down Expand Up @@ -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, {
Expand Down
64 changes: 45 additions & 19 deletions lib/src/helpers/authentication-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -43,6 +45,7 @@ import {
FetchResponse,
OIDCEndpointsInternal,
OIDCProviderMetaData,
TemporaryData,
TokenResponse
} from "../models";
import { AuthenticationUtils } from "../utils";
Expand Down Expand Up @@ -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 ]
: "";
});
}
Expand All @@ -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 };
Expand All @@ -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."
)
);
}
Expand All @@ -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.
Expand All @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 }`;
}
}
1 change: 1 addition & 0 deletions lib/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from "./constants/parameters";
export * from "./constants/data";
export * from "./constants/parameters";
export * from "./constants/scopes";
export * from "./utils";
21 changes: 21 additions & 0 deletions lib/src/utils/authentication-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* under the License.
*/

import { PKCE_CODE_VERIFIER, PKCE_SEPARATOR } from "../constants";
import { DecodedIDTokenPayload } from "../models";

export class AuthenticationUtils {
Expand Down Expand Up @@ -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 }`;
}
}

0 comments on commit 07190e8

Please sign in to comment.