fix(oauth-provider): properly handle metadata field in client registration (#7232)

This commit is contained in:
Paola Estefanía de Campos
2026-01-28 17:06:03 -08:00
committed by Alex Yang
parent d598bd03f7
commit 33015e5f89
5 changed files with 66 additions and 8 deletions
+2 -1
View File
@@ -17,6 +17,7 @@ import {
getClient,
getJwtPlugin,
getStoredToken,
parseClientMetadata,
validateClientCredentials,
} from "./utils";
@@ -218,7 +219,7 @@ async function validateOpaqueAccessToken(
user,
scopes: accessToken.scopes,
referenceId: accessToken?.referenceId,
metadata: client?.metadata ? JSON.parse(client.metadata) : undefined,
metadata: parseClientMetadata(client?.metadata),
})
: {};
@@ -210,6 +210,41 @@ describe("oauth register", async () => {
expect(response.data?.disabled).toBeFalsy();
});
it("should register client with metadata field", async () => {
const response = await auth.api.adminCreateOAuthClient({
headers,
body: {
redirect_uris: [redirectUri],
metadata: {
foo: "bar",
nested: { key: "value" },
},
},
});
expect(response?.client_id).toBeDefined();
expect(response?.client_secret).toBeDefined();
// Metadata should be spread at the top level of the response
expect(response?.foo).toBe("bar");
expect(response?.nested).toEqual({ key: "value" });
});
it("should register client with metadata and other extra fields", async () => {
const response = await auth.api.adminCreateOAuthClient({
headers,
body: {
redirect_uris: [redirectUri],
metadata: {
fromMetadata: "value1",
},
customField: "value2",
} as any,
});
expect(response?.client_id).toBeDefined();
// Both metadata and extra fields should be spread at the top level
expect(response?.fromMetadata).toBe("value1");
expect(response?.customField).toBe("value2");
});
});
describe("oauth register - unauthenticated", async () => {
+12 -4
View File
@@ -4,7 +4,7 @@ import { generateRandomString } from "better-auth/crypto";
import { toExpJWT } from "better-auth/plugins";
import type { OAuthOptions, SchemaClient, Scope } from "./types";
import type { OAuthClient } from "./types/oauth";
import { storeClientSecret } from "./utils";
import { parseClientMetadata, storeClientSecret } from "./utils";
export async function registerEndpoint(
ctx: GenericEndpointContext,
@@ -252,6 +252,7 @@ export function oauthToSchema(input: OAuthClient): SchemaClient<Scope[]> {
skip_consent: skipConsent,
enable_end_session: enableEndSession,
reference_id: referenceId,
metadata: inputMetadata,
// All other metadata
...rest
} = input;
@@ -260,8 +261,15 @@ export function oauthToSchema(input: OAuthClient): SchemaClient<Scope[]> {
const expiresAt = _expiresAt ? new Date(_expiresAt * 1000) : undefined;
const createdAt = _createdAt ? new Date(_createdAt * 1000) : undefined;
const scopes = _scope?.split(" ");
const metadata =
rest && Object.keys(rest).length ? JSON.stringify(rest) : undefined;
const metadataObj = {
...(rest && Object.keys(rest).length ? rest : {}),
...(inputMetadata && typeof inputMetadata === "object"
? inputMetadata
: {}),
};
const metadata = Object.keys(metadataObj).length
? JSON.stringify(metadataObj)
: undefined;
return {
// Important Fields
@@ -354,7 +362,7 @@ export function schemaToOAuth(input: SchemaClient<Scope[]>): OAuthClient {
? Math.round(createdAt.getTime() / 1000)
: undefined;
const _scopes = scopes?.join(" ");
const _metadata = metadata ? JSON.parse(metadata) : undefined;
const _metadata = parseClientMetadata(metadata);
return {
// All other metadata
+4 -3
View File
@@ -20,6 +20,7 @@ import {
decryptStoredClientSecret,
getJwtPlugin,
getStoredToken,
parseClientMetadata,
storeToken,
validateClientCredentials,
} from "./utils";
@@ -85,7 +86,7 @@ async function createJwtAccessToken(
scopes,
resource: ctx.body.resource,
referenceId,
metadata: client.metadata ? JSON.parse(client.metadata) : undefined,
metadata: parseClientMetadata(client.metadata),
})
: {};
@@ -142,7 +143,7 @@ async function createIdToken(
? await opts.customIdTokenClaims({
user,
scopes,
metadata: client.metadata ? JSON.parse(client.metadata) : undefined,
metadata: parseClientMetadata(client.metadata),
})
: {};
@@ -817,7 +818,7 @@ async function handleClientCredentialsGrant(
? await opts.customAccessTokenClaims({
scopes: requestedScopes,
resource: ctx.body.resource,
metadata: client.metadata ? JSON.parse(client.metadata) : undefined,
metadata: parseClientMetadata(client.metadata),
})
: {};
@@ -374,6 +374,19 @@ export async function validateClientCredentials(
return client;
}
/**
* Parse client metadata that may be stored as JSON string or already parsed object.
* Handles database adapters that auto-parse JSON columns.
*
* @internal
*/
export function parseClientMetadata(
metadata: string | object | undefined,
): object | undefined {
if (!metadata) return undefined;
return typeof metadata === "string" ? JSON.parse(metadata) : metadata;
}
/**
* Parse space-separated prompt string into a set of prompts
*