fix(oauth-provider): report an unknown client_id as not found (#11799)

This commit is contained in:
Gustavo Valverde
2026-10-15 09:18:07 +00:00
committed by GitHub
parent 2fa7e2f8bb
commit f42c0c220d
5 changed files with 59 additions and 2 deletions
@@ -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.
@@ -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");
+1 -1
View File
@@ -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) {
+25
View File
@@ -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<Record<string, unknown>>(
"/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,
+1 -1
View File
@@ -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 });