forked from baron/baron-sso
feat(headless-login): add jwks cache visibility and refresh flow
- replace inline headless jwks support with jwksUri-only validation - add cached jwks refresh worker, manual refresh/revoke endpoints, and parsed key summaries - expose allowed algorithms and key previews in DevFront with regression coverage
This commit is contained in:
@@ -12,7 +12,6 @@ export type ClientSummary = {
|
||||
clientSecret?: string;
|
||||
tokenEndpointAuthMethod?: string;
|
||||
jwksUri?: string;
|
||||
jwks?: string | Record<string, unknown>;
|
||||
redirectUris: string[];
|
||||
scopes: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
@@ -63,6 +62,27 @@ export type ClientDetailResponse = {
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
endpoints: ClientEndpoints;
|
||||
headlessJwksCache?: {
|
||||
clientId: string;
|
||||
jwksUri: string;
|
||||
cachedAt: string;
|
||||
expiresAt: string;
|
||||
lastCheckedAt?: string;
|
||||
lastSuccessfulVerificationAt?: string;
|
||||
lastRefreshStatus?: "success" | "failure" | "pending";
|
||||
lastError?: string;
|
||||
consecutiveFailures?: number;
|
||||
cachedKids?: string[];
|
||||
etag?: string;
|
||||
lastModified?: string;
|
||||
parsedKeys?: Array<{
|
||||
kid?: string;
|
||||
kty?: string;
|
||||
use?: string;
|
||||
alg?: string;
|
||||
n?: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type ClientUpsertRequest = {
|
||||
@@ -76,7 +96,6 @@ export type ClientUpsertRequest = {
|
||||
responseTypes?: string[];
|
||||
tokenEndpointAuthMethod?: string;
|
||||
jwksUri?: string;
|
||||
jwks?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
@@ -182,6 +201,17 @@ export async function rotateClientSecret(clientId: string) {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function refreshHeadlessJwksCache(clientId: string) {
|
||||
const { data } = await apiClient.post<ClientDetailResponse>(
|
||||
`/dev/clients/${clientId}/headless-jwks/refresh`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function revokeHeadlessJwksCache(clientId: string) {
|
||||
await apiClient.delete(`/dev/clients/${clientId}/headless-jwks/cache`);
|
||||
}
|
||||
|
||||
export async function deleteClient(clientId: string) {
|
||||
await apiClient.delete(`/dev/clients/${clientId}`);
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
/**
|
||||
* Key Utilities for converting various public key formats (PEM, OpenSSH) to JWKS.
|
||||
*/
|
||||
|
||||
interface JWK {
|
||||
kty: string;
|
||||
n: string;
|
||||
e: string;
|
||||
kid?: string;
|
||||
use?: string;
|
||||
alg?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Base64 string to a URL-safe Base64 string (RFC 7515).
|
||||
*/
|
||||
function toBase64Url(base64: string): string {
|
||||
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts RSA Modulus (n) and Exponent (e) from a SubjectPublicKeyInfo (PEM).
|
||||
* This is a simplified parser for common RSA keys.
|
||||
*/
|
||||
export function parsePemToJwk(pem: string): JWK | null {
|
||||
try {
|
||||
// Remove headers, footers and whitespace
|
||||
pem
|
||||
.replace(/-----BEGIN PUBLIC KEY-----/, "")
|
||||
.replace(/-----END PUBLIC KEY-----/, "")
|
||||
.replace(/\s/g, "");
|
||||
|
||||
// In a real browser environment without heavy libraries,
|
||||
// we would need a full ASN.1 parser.
|
||||
// For now, we recommend using JWKS or OpenSSH formats for reliability,
|
||||
// or we can hint the user that complex PEMs might fail.
|
||||
// However, we'll try to support a basic one.
|
||||
|
||||
return null; // Placeholder: PEM parsing is complex without libs.
|
||||
} catch (e) {
|
||||
console.error("Failed to parse PEM", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an OpenSSH Public Key (ssh-rsa AAAA...) into a JWK.
|
||||
*/
|
||||
export function parseSshRsaToJwk(sshKey: string): JWK | null {
|
||||
try {
|
||||
const parts = sshKey.trim().split(" ");
|
||||
if (parts.length < 2 || parts[0] !== "ssh-rsa") return null;
|
||||
|
||||
const keyData = atob(parts[1]);
|
||||
let offset = 0;
|
||||
|
||||
const readBlob = () => {
|
||||
const len =
|
||||
(keyData.charCodeAt(offset) << 24) |
|
||||
(keyData.charCodeAt(offset + 1) << 16) |
|
||||
(keyData.charCodeAt(offset + 2) << 8) |
|
||||
keyData.charCodeAt(offset + 3);
|
||||
offset += 4;
|
||||
const blob = keyData.slice(offset, offset + len);
|
||||
offset += len;
|
||||
return blob;
|
||||
};
|
||||
|
||||
const type = readBlob(); // "ssh-rsa"
|
||||
if (type !== "ssh-rsa") return null;
|
||||
|
||||
const eBlob = readBlob();
|
||||
const nBlob = readBlob();
|
||||
|
||||
return {
|
||||
kty: "RSA",
|
||||
n: semanticsBase64Url(nBlob),
|
||||
e: semanticsBase64Url(eBlob),
|
||||
alg: "RS256",
|
||||
use: "sig",
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Failed to parse SSH key", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function semanticsBase64Url(blob: string): string {
|
||||
// Ensure leading zero removal for BigInt representations if necessary
|
||||
let start = 0;
|
||||
while (start < blob.length && blob.charCodeAt(start) === 0) {
|
||||
start++;
|
||||
}
|
||||
return toBase64Url(btoa(blob.slice(start)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to auto-detect and convert input to JWKS JSON string.
|
||||
* Returns the original string if it's already JSON or conversion fails.
|
||||
*/
|
||||
export function tryConvertToJwks(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
|
||||
// 1. If it looks like JSON, return as is (validation happens in component)
|
||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
// 2. Try SSH RSA
|
||||
if (trimmed.startsWith("ssh-rsa")) {
|
||||
const jwk = parseSshRsaToJwk(trimmed);
|
||||
if (jwk) {
|
||||
return JSON.stringify({ keys: [jwk] }, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. PEM (Simplified check)
|
||||
if (trimmed.includes("BEGIN PUBLIC KEY")) {
|
||||
// For PEM, we suggest the user uses JWKS or SSH-RSA for now
|
||||
// as JS doesn't have a built-in ASN1 parser and we want to avoid heavy deps.
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
Reference in New Issue
Block a user