fix(oauth-provider): return 400 invalid_grant on PKCE verifier mismatch (#11724)

Co-authored-by: Gustavo Valverde <g.valverde02@gmail.com>
This commit is contained in:
Drew
2026-10-15 13:07:53 +01:00
committed by GitHub
co-authored by Gustavo Valverde
parent 8f3da46ae5
commit 4d7da0bef8
3 changed files with 27 additions and 7 deletions
+5
View File
@@ -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.
@@ -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",
);
+4 -4
View File
@@ -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",
});
}
}