mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 00:22:43 -05:00
feat(oidc): support private_key_jwt
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<JWK[]> {
|
||||
// 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<ReturnType<typeof importJWK>>;
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user