From 5c033bf3572fc6c79300b9fa1cd7e5d1cd2a5374 Mon Sep 17 00:00:00 2001 From: Harry Yep Date: Tue, 2 Dec 2025 16:49:20 +0800 Subject: [PATCH] feat(oidc): support private_key_jwt --- packages/better-auth/src/plugins/mcp/index.ts | 17 ++++++ .../src/plugins/oidc-provider/index.ts | 13 ++++ .../src/plugins/oidc-provider/utils.ts | 59 +++++++++++++++---- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/packages/better-auth/src/plugins/mcp/index.ts b/packages/better-auth/src/plugins/mcp/index.ts index c2147d5291..e801712a11 100644 --- a/packages/better-auth/src/plugins/mcp/index.ts +++ b/packages/better-auth/src/plugins/mcp/index.ts @@ -904,6 +904,19 @@ export const mcp = (options: MCPOptions) => { } } + // Validate private_key_jwt requires jwks or jwks_uri + if ( + body.token_endpoint_auth_method === "private_key_jwt" && + !body.jwks && + !body.jwks_uri + ) { + throw new APIError("BAD_REQUEST", { + error: "invalid_client_metadata", + error_description: + "When 'private_key_jwt' authentication method is used, either 'jwks' or 'jwks_uri' must be provided", + }); + } + const clientId = opts.generateClientId?.() || generateRandomString(32, "a-z", "A-Z"); const clientSecret = @@ -927,6 +940,10 @@ export const mcp = (options: MCPOptions) => { type: clientType, authenticationScheme: body.token_endpoint_auth_method || "client_secret_basic", + jwks: body.jwks ? JSON.stringify(body.jwks) : undefined, + jwksUri: body.jwks_uri, + tokenEndpointAuthMethod: + body.token_endpoint_auth_method || "client_secret_basic", disabled: false, userId: session?.session.userId, createdAt: new Date(), diff --git a/packages/better-auth/src/plugins/oidc-provider/index.ts b/packages/better-auth/src/plugins/oidc-provider/index.ts index 7bc2ad5e2e..36039c69c4 100644 --- a/packages/better-auth/src/plugins/oidc-provider/index.ts +++ b/packages/better-auth/src/plugins/oidc-provider/index.ts @@ -1486,6 +1486,19 @@ export const oidcProvider = (options: OIDCOptions) => { } } + // Validate private_key_jwt requires jwks or jwks_uri + if ( + body.token_endpoint_auth_method === "private_key_jwt" && + !body.jwks && + !body.jwks_uri + ) { + throw new APIError("BAD_REQUEST", { + error: "invalid_client_metadata", + error_description: + "When 'private_key_jwt' authentication method is used, either 'jwks' or 'jwks_uri' must be provided", + }); + } + const clientId = options.generateClientId?.() || generateRandomString(32, "a-z", "A-Z"); diff --git a/packages/better-auth/src/plugins/oidc-provider/utils.ts b/packages/better-auth/src/plugins/oidc-provider/utils.ts index a49ae61c2b..ff3f3ec2bf 100644 --- a/packages/better-auth/src/plugins/oidc-provider/utils.ts +++ b/packages/better-auth/src/plugins/oidc-provider/utils.ts @@ -26,30 +26,49 @@ async function fetchJwksFromUri( jwksUri: string, ctx: GenericEndpointContext, ): Promise<{ keys: unknown[] }> { + let response: Response; try { - const response = await fetch(jwksUri, { + response = await fetch(jwksUri, { headers: { Accept: "application/json", }, }); - if (!response.ok) { - throw new Error(`Failed to fetch JWKS: ${response.status}`); - } - const jwks = await response.json(); - if (!jwks.keys || !Array.isArray(jwks.keys)) { - throw new Error("Invalid JWKS format: missing keys array"); - } - return jwks; } catch (error) { ctx.context.logger.error("Failed to fetch JWKS from URI", { jwksUri, error, }); throw new APIError("UNAUTHORIZED", { - error_description: `failed to fetch jwks from uri: ${error instanceof Error ? error.message : "unknown error"}`, + error_description: `failed to fetch jwks from uri: ${error instanceof Error ? error.message : "network error"}`, error: "invalid_client", }); } + + if (!response.ok) { + throw new APIError("UNAUTHORIZED", { + error_description: `failed to fetch jwks from uri: HTTP ${response.status}`, + error: "invalid_client", + }); + } + + let jwks: { keys?: unknown[] }; + try { + jwks = await response.json(); + } catch { + throw new APIError("UNAUTHORIZED", { + error_description: "failed to fetch jwks from uri: invalid JSON response", + error: "invalid_client", + }); + } + + if (!jwks.keys || !Array.isArray(jwks.keys)) { + throw new APIError("UNAUTHORIZED", { + error_description: "failed to fetch jwks from uri: missing keys array", + error: "invalid_client", + }); + } + + return { keys: jwks.keys }; } /** @@ -61,7 +80,15 @@ async function getClientJwksKeys( ): Promise { // First try inline JWKS if (client.jwks) { - const jwks = JSON.parse(client.jwks); + let jwks: { keys?: unknown[] }; + try { + jwks = JSON.parse(client.jwks); + } catch { + throw new APIError("UNAUTHORIZED", { + error_description: "invalid jwks format: malformed JSON", + error: "invalid_client", + }); + } if (jwks.keys && Array.isArray(jwks.keys) && jwks.keys.length > 0) { return jwks.keys as JWK[]; } @@ -128,7 +155,15 @@ export async function verifyClientAssertion(params: { key = foundKey; } - const publicKey = await importJWK(key); + let publicKey: Awaited>; + try { + publicKey = await importJWK(key); + } catch (err) { + throw new APIError("UNAUTHORIZED", { + error_description: `failed to import jwk: ${err instanceof Error ? err.message : "invalid key format"}`, + error: "invalid_client", + }); + } const { payload } = await jwtVerify(clientAssertion, publicKey, { issuer: clientId,