Adds enterprise-managed authorization (EMA): browser-side IdP login + RFC 8693 token-exchange and JWT-bearer resource-token mint, per-server EMA opt-in in Server Settings, IdP config in Client Settings, and IdP clientSecret keychain storage. Includes the v2/main merge and review follow-ups (400 on malformed client config; normalizeIdpIssuer import consolidation).
70 lines
1.9 KiB
TypeScript
70 lines
1.9 KiB
TypeScript
function decodeBase64Url(segment: string): string {
|
|
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
const padded = base64.padEnd(
|
|
base64.length + ((4 - (base64.length % 4)) % 4),
|
|
"=",
|
|
);
|
|
const binary = atob(padded);
|
|
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
return new TextDecoder().decode(bytes);
|
|
}
|
|
|
|
/** Best-effort `exp` (epoch seconds) from an unparsed JWT payload. */
|
|
export function jwtExpiresAtMs(token: string): number | undefined {
|
|
const parts = token.split(".");
|
|
if (parts.length < 2) return undefined;
|
|
try {
|
|
const payload = JSON.parse(decodeBase64Url(parts[1]!)) as { exp?: number };
|
|
if (typeof payload.exp === "number" && Number.isFinite(payload.exp)) {
|
|
return payload.exp * 1000;
|
|
}
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function isJwtExpired(
|
|
token: string,
|
|
skewMs = 60_000,
|
|
nowMs = Date.now(),
|
|
): boolean {
|
|
const exp = jwtExpiresAtMs(token);
|
|
if (exp === undefined) return false;
|
|
return nowMs >= exp - skewMs;
|
|
}
|
|
|
|
/** True when `token` has the three-segment JWT shape (no signature verification). */
|
|
export function isJwtFormat(token: string): boolean {
|
|
const parts = token.split(".");
|
|
return (
|
|
parts.length === 3 &&
|
|
parts[0]!.length > 0 &&
|
|
parts[1]!.length > 0 &&
|
|
parts[2] !== undefined
|
|
);
|
|
}
|
|
|
|
/** Best-effort decode of JWT header and payload for display (no verification). */
|
|
export function decodeJwtPayload(token: string):
|
|
| {
|
|
header: Record<string, unknown>;
|
|
payload: Record<string, unknown>;
|
|
}
|
|
| undefined {
|
|
if (!isJwtFormat(token)) return undefined;
|
|
const parts = token.split(".");
|
|
try {
|
|
const header = JSON.parse(decodeBase64Url(parts[0]!)) as Record<
|
|
string,
|
|
unknown
|
|
>;
|
|
const payload = JSON.parse(decodeBase64Url(parts[1]!)) as Record<
|
|
string,
|
|
unknown
|
|
>;
|
|
return { header, payload };
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|