From f42c0c220d42fbfcb672c492e5ef1d2b29da3d2f Mon Sep 17 00:00:00 2001 From: Gustavo Valverde Date: Thu, 15 Oct 2026 09:18:07 +0000 Subject: [PATCH] fix(oauth-provider): report an unknown client_id as not found (#11799) --- ...uth-provider-unknown-client-description.md | 5 ++++ packages/oauth-provider/src/authorize.test.ts | 27 +++++++++++++++++++ packages/oauth-provider/src/authorize.ts | 2 +- packages/oauth-provider/src/token.test.ts | 25 +++++++++++++++++ packages/oauth-provider/src/utils/index.ts | 2 +- 5 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 .changeset/oauth-provider-unknown-client-description.md diff --git a/.changeset/oauth-provider-unknown-client-description.md b/.changeset/oauth-provider-unknown-client-description.md new file mode 100644 index 0000000000..cd2aec76e9 --- /dev/null +++ b/.changeset/oauth-provider-unknown-client-description.md @@ -0,0 +1,5 @@ +--- +"@better-auth/oauth-provider": patch +--- + +Requests that name a `client_id` the server does not know now fail with `error_description` set to `client not found`. Before, `/oauth2/authorize` said `client_id is required`, which read as a missing parameter, and `/oauth2/token`, `/oauth2/revoke`, and `/oauth2/introspect` said `missing client`. You would see this, for example, when an MCP client kept a registration the server had deleted. The `invalid_client` error code is unchanged. diff --git a/packages/oauth-provider/src/authorize.test.ts b/packages/oauth-provider/src/authorize.test.ts index 5956ba28d3..dca0ec4b1b 100644 --- a/packages/oauth-provider/src/authorize.test.ts +++ b/packages/oauth-provider/src/authorize.test.ts @@ -295,6 +295,33 @@ describe("oauth authorize - unauthenticated", async () => { ); }); + /** + * @see https://github.com/croffasia/itsaplan/issues/564 + */ + it("reports an unknown client_id as a client that was not found", async () => { + const authUrl = new URL(`${authServerBaseUrl}/api/auth/oauth2/authorize`); + authUrl.search = new URLSearchParams({ + response_type: "code", + client_id: "no-such-client", + redirect_uri: redirectUri, + code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + code_challenge_method: "S256", + }).toString(); + + let location = ""; + await unauthenticatedClient.$fetch(authUrl.toString(), { + onError(context) { + location = context.response.headers.get("Location") || ""; + }, + }); + const errorUrl = new URL(location, authServerBaseUrl); + expect(errorUrl.origin).toBe(authServerBaseUrl); + expect(errorUrl.searchParams.get("error")).toBe("invalid_client"); + expect(errorUrl.searchParams.get("error_description")).toBe( + "client not found", + ); + }); + it("should replace incoming sig when signing the login redirect", async () => { if (!oauthClient?.client_id || !oauthClient?.client_secret) { throw Error("beforeAll not run properly"); diff --git a/packages/oauth-provider/src/authorize.ts b/packages/oauth-provider/src/authorize.ts index 5d001d3679..bbb32090b0 100644 --- a/packages/oauth-provider/src/authorize.ts +++ b/packages/oauth-provider/src/authorize.ts @@ -566,7 +566,7 @@ export async function authorizeEndpoint( if (!client) { return handleRedirect( ctx, - getErrorURL(ctx, "invalid_client", "client_id is required"), + getErrorURL(ctx, "invalid_client", "client not found"), ); } if (client.disabled) { diff --git a/packages/oauth-provider/src/token.test.ts b/packages/oauth-provider/src/token.test.ts index 66fc4c47f5..136791e624 100644 --- a/packages/oauth-provider/src/token.test.ts +++ b/packages/oauth-provider/src/token.test.ts @@ -2369,6 +2369,31 @@ describe("oauth token - client_credentials", async () => { ).toContain("registered for client_secret_basic"); }); + /** + * @see https://github.com/croffasia/itsaplan/issues/564 + */ + it("reports an unknown client_id as a client that was not found", async () => { + const response = await client.$fetch>( + "/oauth2/token", + { + method: "POST", + body: new URLSearchParams({ + grant_type: "client_credentials", + client_id: "no-such-client", + client_secret: "no-such-secret", + }), + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + }, + ); + + expect(response.error).toMatchObject({ + error: "invalid_client", + error_description: "client not found", + }); + }); + it("returns 400 for failed client_secret_post authentication", async () => { const postClient = await auth.api.adminCreateOAuthClient({ headers, diff --git a/packages/oauth-provider/src/utils/index.ts b/packages/oauth-provider/src/utils/index.ts index d79e15000d..a5df2c806b 100644 --- a/packages/oauth-provider/src/utils/index.ts +++ b/packages/oauth-provider/src/utils/index.ts @@ -749,7 +749,7 @@ export async function validateClientCredentials( ) { const client = await getClient(ctx, options, clientId); if (!client) { - throwInvalidClient("missing client", { method: authMethod }); + throwInvalidClient("client not found", { method: authMethod }); } if (client.disabled) { throwInvalidClient("client is disabled", { method: authMethod });