From 61c913da73e57594baf360bfddbb1e48b92349fd Mon Sep 17 00:00:00 2001 From: Gustavo Valverde Date: Thu, 15 Oct 2026 13:11:39 +0100 Subject: [PATCH] fix(oauth-provider): report an unknown client in a client assertion as not found (#11811) --- .../oauth-provider-infer-loopback-native.md | 2 +- ...uth-provider-unknown-client-description.md | 2 +- .../src/private-key-jwt.test.ts | 22 +++++++++++++++++++ .../src/utils/client-assertion.ts | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.changeset/oauth-provider-infer-loopback-native.md b/.changeset/oauth-provider-infer-loopback-native.md index e29880d6ac..e9445c1fd0 100644 --- a/.changeset/oauth-provider-infer-loopback-native.md +++ b/.changeset/oauth-provider-infer-loopback-native.md @@ -2,4 +2,4 @@ "@better-auth/oauth-provider": patch --- -With `clientRegistrationDefaultApplicationType: "infer"`, clients that register an `http` callback on `localhost`, `127.0.0.1`, or `[::1]` without `application_type` now register as `native`. This includes older Claude Code releases, which register `http://127.0.0.1:/callback`. Current Claude Code releases send `application_type: "native"` and were not affected. Before, `"infer"` chose `native` only for custom-scheme callbacks, so these registrations were treated as `web` and rejected with `invalid_redirect_uri`. The `"web"` default and the `"native"` setting are unchanged, and an `application_type` the client sends is never overridden. +With `clientRegistrationDefaultApplicationType: "infer"`, clients that register an `http` callback on `localhost`, `127.0.0.1`, or `[::1]` without `application_type` now register as `native`. This includes older Claude Code releases, which register `http://127.0.0.1:/callback`. Current Claude Code releases send `application_type: "native"` and were not affected. Before, `"infer"` chose `native` only for callbacks whose scheme is not `http` or `https`, so these registrations were treated as `web` and rejected with `invalid_redirect_uri`. The `"web"` default and the `"native"` setting are unchanged, and an `application_type` the client sends is never overridden. diff --git a/.changeset/oauth-provider-unknown-client-description.md b/.changeset/oauth-provider-unknown-client-description.md index cd2aec76e9..cb413614a1 100644 --- a/.changeset/oauth-provider-unknown-client-description.md +++ b/.changeset/oauth-provider-unknown-client-description.md @@ -2,4 +2,4 @@ "@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. +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, `/oauth2/token`, `/oauth2/revoke`, and `/oauth2/introspect` said `missing client`, and requests that authenticate with a `private_key_jwt` client assertion said `unknown 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/private-key-jwt.test.ts b/packages/oauth-provider/src/private-key-jwt.test.ts index 2091fcb8e4..5fcc0cec3d 100644 --- a/packages/oauth-provider/src/private-key-jwt.test.ts +++ b/packages/oauth-provider/src/private-key-jwt.test.ts @@ -645,6 +645,28 @@ describe("private_key_jwt authentication", async () => { expect(result.error?.status).toBeLessThan(500); }); + /** + * @see https://github.com/croffasia/itsaplan/issues/564 + */ + it("reports an assertion for an unknown client as a client that was not found", async () => { + const result = await client.$fetch("/oauth2/token", { + method: "POST", + body: new URLSearchParams({ + grant_type: "client_credentials", + client_id: "no-such-client", + client_assertion_type: + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: await signAssertion({ clientId: "no-such-client" }), + }), + headers: { "content-type": "application/x-www-form-urlencoded" }, + }); + + expect(result.error).toMatchObject({ + error: "invalid_client", + error_description: "client not found", + }); + }); + it("should reject expired assertion", async () => { const now = Math.floor(Date.now() / 1000); const codeVerifier = generateRandomString(32); diff --git a/packages/oauth-provider/src/utils/client-assertion.ts b/packages/oauth-provider/src/utils/client-assertion.ts index a38cfda9b4..a2b9170937 100644 --- a/packages/oauth-provider/src/utils/client-assertion.ts +++ b/packages/oauth-provider/src/utils/client-assertion.ts @@ -532,7 +532,7 @@ export async function verifyClientAssertion( const client = await getClient(ctx, opts, clientId); if (!client) { throw new APIError("BAD_REQUEST", { - error_description: "unknown client", + error_description: "client not found", error: "invalid_client", }); }