Skip to content

Commit dc9111e

Browse files
committed
Harden OAuth PKCE session consistency
1 parent 916dd2a commit dc9111e

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
@@ -2236,10 +2236,10 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => {
22362236
}
22372237
}
22382238

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

0 commit comments

Comments
 (0)