diff --git a/.changeset/pkce-token-error-codes.md b/.changeset/pkce-token-error-codes.md new file mode 100644 index 0000000000..12b4e6a087 --- /dev/null +++ b/.changeset/pkce-token-error-codes.md @@ -0,0 +1,5 @@ +--- +"@better-auth/oauth-provider": patch +--- + +Clients exchanging an authorization code with PKCE now get the token errors RFC 7636 and RFC 6749 specify. A `code_verifier` that does not match the authorization request's `code_challenge` returns HTTP 400 `invalid_grant`; it previously returned HTTP 401 `invalid_request`. For clients where PKCE is optional, a missing or unexpected `code_verifier` still returns `invalid_request`, now with HTTP 400 instead of 401. No request that failed before now succeeds. diff --git a/packages/oauth-provider/src/pkce-optional.test.ts b/packages/oauth-provider/src/pkce-optional.test.ts index 26b5f567df..8230597e3d 100644 --- a/packages/oauth-provider/src/pkce-optional.test.ts +++ b/packages/oauth-provider/src/pkce-optional.test.ts @@ -627,6 +627,9 @@ describe("PKCE optional - offline_access scope", async () => { }); }); +/** + * @see https://github.com/better-auth/better-auth/issues/11723 + */ describe("PKCE optional - consistency checks", async () => { const authServerBaseUrl = "http://localhost:3003"; const rpBaseUrl = "https://rp.example.com"; @@ -713,11 +716,14 @@ describe("PKCE optional - consistency checks", async () => { body, headers, onError(context) { - expect(context.response.status).toBe(401); + expect(context.response.status).toBe(400); }, }); expect(tokenResponse.error).toBeDefined(); + expect((tokenResponse.error as { error?: string }).error).toBe( + "invalid_request", + ); expect((tokenResponse.error as any).error_description).toContain( "code_verifier required because PKCE was used in authorization", ); @@ -761,16 +767,22 @@ describe("PKCE optional - consistency checks", async () => { body, headers, onError(context) { - expect(context.response.status).toBe(401); + expect(context.response.status).toBe(400); }, }); expect(tokenResponse.error).toBeDefined(); + expect((tokenResponse.error as { error?: string }).error).toBe( + "invalid_request", + ); expect((tokenResponse.error as any).error_description).toContain( "code_verifier provided but PKCE was not used in authorization", ); }); + /** + * @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6 + */ it("mismatched PKCE challenge should fail", async () => { // Authorize with PKCE const codeVerifier = generateRandomString(64); @@ -819,12 +831,15 @@ describe("PKCE optional - consistency checks", async () => { body, headers, onError(context) { - expect(context.response.status).toBe(401); + expect(context.response.status).toBe(400); }, }, ); expect(tokenResponse.error).toBeDefined(); + expect((tokenResponse.error as { error?: string }).error).toBe( + "invalid_grant", + ); expect((tokenResponse.error as any).error_description).toContain( "code verification failed", ); diff --git a/packages/oauth-provider/src/token.ts b/packages/oauth-provider/src/token.ts index febb182034..c2c6e7a77d 100644 --- a/packages/oauth-provider/src/token.ts +++ b/packages/oauth-provider/src/token.ts @@ -1603,7 +1603,7 @@ async function handleAuthorizationCodeGrant( if (pkceUsedInAuth && !pkceUsedInToken) { // PKCE was used in authorization but not in token exchange - throw new APIError("UNAUTHORIZED", { + throw new APIError("BAD_REQUEST", { error_description: "code_verifier required because PKCE was used in authorization", error: "invalid_request", @@ -1612,7 +1612,7 @@ async function handleAuthorizationCodeGrant( if (!pkceUsedInAuth && pkceUsedInToken) { // PKCE was not used in authorization but verifier provided - throw new APIError("UNAUTHORIZED", { + throw new APIError("BAD_REQUEST", { error_description: "code_verifier provided but PKCE was not used in authorization", error: "invalid_request", @@ -1626,9 +1626,9 @@ async function handleAuthorizationCodeGrant( : undefined; if (challenge !== verificationValue.query?.code_challenge) { - throw new APIError("UNAUTHORIZED", { + throw new APIError("BAD_REQUEST", { error_description: "code verification failed", - error: "invalid_request", + error: "invalid_grant", }); } }