Skip to content

Commit 2ac7f5b

Browse files
committed
Harden OAuth PKCE session consistency
1 parent 31fa075 commit 2ac7f5b

3 files changed

Lines changed: 15 additions & 6 deletions

File tree

‎packages/core/sdk/src/oauth-helpers.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,10 @@ describe("buildAuthorizationUrl", () => {
273273
...withoutPkce,
274274
authorizationUrl:
275275
"https://example.com/authorize?code_challenge=stale&code_challenge_method=S256",
276+
extraParams: {
277+
code_challenge: "also-stale",
278+
code_challenge_method: "plain",
279+
},
276280
}),
277281
);
278282
expect(url.searchParams.has("code_challenge_method")).toBe(false);

‎packages/core/sdk/src/oauth-helpers.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,6 @@ export const buildAuthorizationUrl = (input: BuildAuthorizationUrlInput): string
252252
if (input.codeChallenge) {
253253
url.searchParams.set("code_challenge_method", "S256");
254254
url.searchParams.set("code_challenge", input.codeChallenge);
255-
} else {
256-
url.searchParams.delete("code_challenge_method");
257-
url.searchParams.delete("code_challenge");
258255
}
259256
if (input.resource) {
260257
url.searchParams.set("resource", input.resource);
@@ -264,6 +261,12 @@ export const buildAuthorizationUrl = (input: BuildAuthorizationUrlInput): string
264261
url.searchParams.set(k, v);
265262
}
266263
}
264+
// When this flow does not use PKCE, configured endpoint or provider-extra
265+
// parameters must not reintroduce a challenge without a persisted verifier.
266+
if (!input.codeChallenge) {
267+
url.searchParams.delete("code_challenge_method");
268+
url.searchParams.delete("code_challenge");
269+
}
267270
return url.toString();
268271
};
269272

‎packages/core/sdk/src/oauth-service.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2238,10 +2238,10 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => {
22382238
}
22392239
}
22402240

2241-
const usePkce = shouldUsePkce(client.authorizationUrl, client.clientSecret);
2241+
const requiresPkce = shouldUsePkce(client.authorizationUrl, client.clientSecret);
22422242
// Every authorization-code flow except LinkedIn's confidential web flow
22432243
// requires the verifier minted by `start`. Missing one is a corrupt row.
2244-
if (usePkce && session.pkceVerifier == null) {
2244+
if (requiresPkce && session.pkceVerifier == null) {
22452245
return yield* new OAuthCompleteError({
22462246
message: `OAuth session ${input.state} is missing its PKCE code verifier; restart the flow.`,
22472247
restartRequired: true,
@@ -2263,7 +2263,9 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => {
22632263
clientId: client.clientId,
22642264
clientSecret: client.clientSecret,
22652265
redirectUrl: session.redirectUrl,
2266-
codeVerifier: usePkce ? (session.pkceVerifier ?? undefined) : undefined,
2266+
// The persisted verifier records the request that actually started.
2267+
// Keep using it if client settings change while that request is open.
2268+
codeVerifier: session.pkceVerifier ?? undefined,
22672269
code: input.code,
22682270
clientAuth: client.tokenEndpointAuthMethod,
22692271
requestFormat: client.tokenRequestFormat,

0 commit comments

Comments
 (0)