Skip to content

Commit 07190e8

Browse files
authored
Merge pull request #179 from thivi/master
Correlate authorization request with access token request using the state parameter.
2 parents a9db4f0 + 511d598 commit 07190e8

File tree

7 files changed

+114
-41
lines changed

7 files changed

+114
-41
lines changed

lib/src/client.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,15 @@ export class AsgardeoAuthClient<T> {
197197
public async requestAccessToken(
198198
authorizationCode: string,
199199
sessionState: string,
200+
state: string,
200201
userID?: string
201202
): Promise<TokenResponse> {
202203
if (await this._dataLayer.getTemporaryDataParameter(OP_CONFIG_INITIATED)) {
203-
return this._authenticationCore.requestAccessToken(authorizationCode, sessionState, userID);
204+
return this._authenticationCore.requestAccessToken(authorizationCode, sessionState, state, userID);
204205
}
205206

206207
return this._authenticationCore.getOIDCProviderMetaData(false).then(() => {
207-
return this._authenticationCore.requestAccessToken(authorizationCode, sessionState, userID);
208+
return this._authenticationCore.requestAccessToken(authorizationCode, sessionState, state,userID);
208209
});
209210
}
210211

lib/src/constants/data.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export enum Stores {
2525

2626
export const REFRESH_TOKEN_TIMER = "refresh_token_timer";
2727
export const PKCE_CODE_VERIFIER = "pkce_code_verifier";
28+
export const PKCE_SEPARATOR = "#";
2829

2930
export const SUPPORTED_SIGNATURE_ALGORITHMS = [
3031
"RS256", "RS512", "RS384", "PS256"

lib/src/constants/parameters.ts

+17-16
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
/**
2-
* Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
3-
*
4-
* WSO2 Inc. licenses this file to you under the Apache License,
5-
* Version 2.0 (the "License"); you may not use this file except
6-
* in compliance with the License.
7-
* You may obtain a copy of the License at
8-
*
9-
* http://www.apache.org/licenses/LICENSE-2.0
10-
*
11-
* Unless required by applicable law or agreed to in writing,
12-
* software distributed under the License is distributed on an
13-
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14-
* KIND, either express or implied. See the License for the
15-
* specific language governing permissions and limitations
16-
* under the License.
17-
*/
2+
* Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
3+
*
4+
* WSO2 Inc. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
1818

1919
export const AUTHORIZATION_CODE = "code";
2020
export const SESSION_STATE = "session_state";
2121
export const SIGN_OUT_URL = "sign_out_url";
2222
export const SIGN_OUT_SUCCESS_PARAM = "sign_out_success";
23+
export const STATE = "state";

lib/src/core/authentication-core.ts

+26-4
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
OP_CONFIG_INITIATED,
2323
PKCE_CODE_VERIFIER,
2424
SESSION_STATE,
25-
SIGN_OUT_SUCCESS_PARAM
25+
SIGN_OUT_SUCCESS_PARAM,
26+
STATE
2627
} from "../constants";
2728
import { DataLayer } from "../data";
2829
import { AsgardeoAuthException, AsgardeoAuthNetworkException } from "../exception";
@@ -100,10 +101,13 @@ export class AuthenticationCore<T> {
100101
authorizeRequest.searchParams.append("response_mode", configData.responseMode);
101102
}
102103

104+
const pkceKey: string = await this._authenticationHelper.generatePKCEKey(userID);
105+
103106
if (configData.enablePKCE) {
104107
const codeVerifier = this._cryptoHelper?.getCodeVerifier();
105108
const codeChallenge = this._cryptoHelper?.getCodeChallenge(codeVerifier);
106-
await this._dataLayer.setTemporaryDataParameter(PKCE_CODE_VERIFIER, codeVerifier, userID);
109+
110+
await this._dataLayer.setTemporaryDataParameter(pkceKey, codeVerifier, userID);
107111
authorizeRequest.searchParams.append("code_challenge_method", "S256");
108112
authorizeRequest.searchParams.append("code_challenge", codeChallenge);
109113
}
@@ -121,12 +125,21 @@ export class AuthenticationCore<T> {
121125
}
122126
}
123127

128+
authorizeRequest.searchParams.append(
129+
STATE,
130+
AuthenticationUtils.generateStateParamForRequestCorrelation(
131+
pkceKey,
132+
authorizeRequest.searchParams.get(STATE) ?? ""
133+
)
134+
);
135+
124136
return authorizeRequest.toString();
125137
}
126138

127139
public async requestAccessToken(
128140
authorizationCode: string,
129141
sessionState: string,
142+
state: string,
130143
userID?: string
131144
): Promise<TokenResponse> {
132145
const tokenEndpoint = (await this._oidcProviderMetaData()).token_endpoint;
@@ -161,8 +174,17 @@ export class AuthenticationCore<T> {
161174
body.push(`redirect_uri=${ configData.signInRedirectURL }`);
162175

163176
if (configData.enablePKCE) {
164-
body.push(`code_verifier=${ await this._dataLayer.getTemporaryDataParameter(PKCE_CODE_VERIFIER, userID) }`);
165-
await this._dataLayer.removeTemporaryDataParameter(PKCE_CODE_VERIFIER, userID);
177+
body.push(
178+
`code_verifier=${ await this._dataLayer.getTemporaryDataParameter(
179+
AuthenticationUtils.extractPKCEKeyFromStateParam(state),
180+
userID
181+
) }`
182+
);
183+
184+
await this._dataLayer.removeTemporaryDataParameter(
185+
AuthenticationUtils.extractPKCEKeyFromStateParam(state),
186+
userID
187+
);
166188
}
167189

168190
return fetch(tokenEndpoint, {

lib/src/helpers/authentication-helper.ts

+45-19
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
JWKS_ENDPOINT,
2727
OIDC_SCOPE,
2828
OIDC_SESSION_IFRAME_ENDPOINT,
29+
PKCE_CODE_VERIFIER,
30+
PKCE_SEPARATOR,
2931
REVOKE_TOKEN_ENDPOINT,
3032
SCOPE_TAG,
3133
SERVICE_RESOURCES,
@@ -43,6 +45,7 @@ import {
4345
FetchResponse,
4446
OIDCEndpointsInternal,
4547
OIDCProviderMetaData,
48+
TemporaryData,
4649
TokenResponse
4750
} from "../models";
4851
import { AuthenticationUtils } from "../utils";
@@ -76,9 +79,9 @@ export class AuthenticationHelper<T> {
7679
if (configData.overrideWellEndpointConfig) {
7780
configData.endpoints &&
7881
Object.keys(configData.endpoints).forEach((endpointName: string) => {
79-
const snakeCasedName = endpointName.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
80-
oidcProviderMetaData[snakeCasedName] = configData?.endpoints
81-
? configData.endpoints[endpointName]
82+
const snakeCasedName = endpointName.replace(/[A-Z]/g, (letter) => `_${ letter.toLowerCase() }`);
83+
oidcProviderMetaData[ snakeCasedName ] = configData?.endpoints
84+
? configData.endpoints[ endpointName ]
8285
: "";
8386
});
8487
}
@@ -92,17 +95,17 @@ export class AuthenticationHelper<T> {
9295

9396
configData.endpoints &&
9497
Object.keys(configData.endpoints).forEach((endpointName: string) => {
95-
const snakeCasedName = endpointName.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
96-
oidcProviderMetaData[snakeCasedName] = configData?.endpoints ? configData.endpoints[endpointName] : "";
98+
const snakeCasedName = endpointName.replace(/[A-Z]/g, (letter) => `_${ letter.toLowerCase() }`);
99+
oidcProviderMetaData[ snakeCasedName ] = configData?.endpoints ? configData.endpoints[ endpointName ] : "";
97100
});
98101

99102
const defaultEndpoints = {
100-
[AUTHORIZATION_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.authorizationEndpoint,
101-
[END_SESSION_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.endSessionEndpoint,
102-
[JWKS_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.jwksUri,
103-
[OIDC_SESSION_IFRAME_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.checkSessionIframe,
104-
[REVOKE_TOKEN_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.revocationEndpoint,
105-
[TOKEN_ENDPOINT]: configData.serverOrigin + SERVICE_RESOURCES.tokenEndpoint
103+
[ AUTHORIZATION_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.authorizationEndpoint,
104+
[ END_SESSION_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.endSessionEndpoint,
105+
[ JWKS_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.jwksUri,
106+
[ OIDC_SESSION_IFRAME_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.checkSessionIframe,
107+
[ REVOKE_TOKEN_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.revocationEndpoint,
108+
[ TOKEN_ENDPOINT ]: configData.serverOrigin + SERVICE_RESOURCES.tokenEndpoint
106109
};
107110

108111
return { ...oidcProviderMetaData, ...defaultEndpoints };
@@ -120,7 +123,7 @@ export class AuthenticationHelper<T> {
120123
"validateIdToken",
121124
"JWKS endpoint not found.",
122125
"No JWKS endpoint was found in the OIDC provider meta data returned by the well-known endpoint " +
123-
"or the JWKS endpoint passed to the SDK is empty."
126+
"or the JWKS endpoint passed to the SDK is empty."
124127
)
125128
);
126129
}
@@ -145,7 +148,7 @@ export class AuthenticationHelper<T> {
145148
}
146149

147150
const issuer = (await this._oidcProviderMetaData()).issuer;
148-
const issuerFromURL = (await this.resolveWellKnownEndpoint()).split("/.well-known")[0];
151+
const issuerFromURL = (await this.resolveWellKnownEndpoint()).split("/.well-known")[ 0 ];
149152

150153
// Return false if the issuer in the open id config doesn't match
151154
// the issuer in the well known endpoint URL.
@@ -155,7 +158,7 @@ export class AuthenticationHelper<T> {
155158
const parsedResponse = await response.json();
156159

157160
return this._cryptoHelper
158-
.getJWKForTheIdToken(idToken.split(".")[0], parsedResponse.keys)
161+
.getJWKForTheIdToken(idToken.split(".")[ 0 ], parsedResponse.keys)
159162
.then(async (jwk: any) => {
160163
return this._cryptoHelper
161164
.isValidIdToken(
@@ -218,12 +221,12 @@ export class AuthenticationHelper<T> {
218221
const familyName: string = payload.family_name ?? "";
219222
const fullName: string =
220223
givenName && familyName
221-
? `${givenName} ${familyName}`
224+
? `${ givenName } ${ familyName }`
222225
: givenName
223-
? givenName
224-
: familyName
225-
? familyName
226-
: "";
226+
? givenName
227+
: familyName
228+
? familyName
229+
: "";
227230
const displayName: string = payload.preferred_username ?? fullName;
228231

229232
return {
@@ -329,4 +332,27 @@ export class AuthenticationHelper<T> {
329332
return Promise.resolve(tokenResponse);
330333
}
331334
}
335+
336+
/**
337+
* This generates a PKCE key with the right index value.
338+
*
339+
* @param {string} userID The userID to identify a user in a multi-user scenario.
340+
*
341+
* @returns {string} The PKCE key.
342+
*/
343+
public async generatePKCEKey(userID?: string): Promise<string> {
344+
const tempData: TemporaryData = await this._dataLayer.getTemporaryData(userID);
345+
const keys: string[] = [];
346+
347+
Object.keys(tempData).forEach((key: string) => {
348+
if (key.startsWith(PKCE_CODE_VERIFIER)) {
349+
keys.push(key);
350+
}
351+
});
352+
353+
const lastKey: string | undefined = keys.sort().pop();
354+
const index: number = parseInt(lastKey?.split(PKCE_SEPARATOR)[ 1 ] ?? "-1");
355+
356+
return `${ PKCE_CODE_VERIFIER }${ PKCE_SEPARATOR }${ index + 1 }`;
357+
}
332358
}

lib/src/public-api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export * from "./constants/parameters";
2424
export * from "./constants/data";
2525
export * from "./constants/parameters";
2626
export * from "./constants/scopes";
27+
export * from "./utils";

lib/src/utils/authentication-utils.ts

+21
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* under the License.
1717
*/
1818

19+
import { PKCE_CODE_VERIFIER, PKCE_SEPARATOR } from "../constants";
1920
import { DecodedIDTokenPayload } from "../models";
2021

2122
export class AuthenticationUtils {
@@ -78,4 +79,24 @@ export class AuthenticationUtils {
7879
"Content-Type": "application/x-www-form-urlencoded"
7980
};
8081
}
82+
83+
/**
84+
* This generates the state param value to be sent with an authorization request.
85+
*
86+
* @param {string} pkceKey The PKCE key.
87+
* @param {string} state The state value to be passed. (The correlation ID will be appended to this state value.)
88+
*
89+
* @returns {string} The state param value.
90+
*/
91+
public static generateStateParamForRequestCorrelation(pkceKey: string, state?: string): string {
92+
const index: number = parseInt(pkceKey.split(PKCE_SEPARATOR)[ 1 ]);
93+
94+
return state ? `${ state }_request_${ index }` : `request_${ index }`;
95+
}
96+
97+
public static extractPKCEKeyFromStateParam(stateParam: string): string {
98+
const index: number = parseInt(stateParam.split("request_")[ 1 ]);
99+
100+
return `${ PKCE_CODE_VERIFIER }${ PKCE_SEPARATOR }${ index }`;
101+
}
81102
}

0 commit comments

Comments
 (0)