feat(oidc): support private_key_jwt

This commit is contained in:
Harry Yep
2025-12-02 16:49:20 +08:00
parent 5537d19b41
commit 5c033bf357
3 changed files with 77 additions and 12 deletions

View File

@@ -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(),

View File

@@ -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");

View File

@@ -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,