mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 23:53:17 -05:00
fix(oauth-provider): properly handle metadata field in client registration (#7232)
This commit is contained in:
committed by
Alex Yang
parent
d598bd03f7
commit
33015e5f89
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user