fix(auth): make diagnostics match runtime safety

This commit is contained in:
2026-08-17 11:23:52 +02:00
parent f0ae680671
commit 7cfbee36fa
14 changed files with 619 additions and 99 deletions
+36 -3
View File
@@ -48,6 +48,14 @@ export class OidcJwksUnavailableError extends OidcProtocolError {
}
}
/** Discovery completed with metadata for a different issuer than the configured trust anchor. */
export class OidcIssuerMismatchError extends OidcProtocolError {
constructor() {
super("oidc_issuer_mismatch");
this.name = "OidcIssuerMismatchError";
}
}
export interface OidcProtocolOptions {
issuer: string;
clientId: string;
@@ -373,7 +381,14 @@ function availabilityFailure(error: unknown): boolean {
}
function protocolFailure(error: unknown): OidcProtocolError {
if (error instanceof OidcProtocolError) return error;
let current = error;
const seen = new Set<object>();
for (let depth = 0; depth < 8; depth += 1) {
if (current instanceof OidcProtocolError) return current;
if (!current || typeof current !== "object" || seen.has(current)) break;
seen.add(current);
current = (current as { cause?: unknown }).cause;
}
return availabilityFailure(error) ? new OidcProviderUnavailableError() : new OidcProtocolError();
}
@@ -463,22 +478,40 @@ export function createOidcProtocol(options: OidcProtocolOptions): OidcProtocol {
const configuration = async (): Promise<Configuration> => {
if (!discovered) {
discovered = (async () => {
let inspectingDiscovery = true;
const issuerCheckingFetch: CustomFetch = async (input, init) => {
const response = await transport.customFetch(input, init);
if (inspectingDiscovery) {
try {
const metadata = await response.clone().json() as { issuer?: unknown };
if (typeof metadata?.issuer === "string" && metadata.issuer !== options.issuer) {
throw new OidcIssuerMismatchError();
}
} catch (error) {
if (error instanceof OidcIssuerMismatchError) throw error;
// The OIDC library owns all other discovery-document validation.
}
}
return response;
};
try {
const config = await discovery(
issuerUrl,
options.clientId,
{ client_secret: options.clientSecret, redirect_uris: [callbackUrl.href], response_types: ["code"] },
undefined,
{ [customFetch]: transport.customFetch, timeout: httpTimeoutMs / 1000 },
{ [customFetch]: issuerCheckingFetch, timeout: httpTimeoutMs / 1000 },
);
const metadata = config.serverMetadata();
if (metadata.issuer !== options.issuer) throw new OidcProtocolError();
if (metadata.issuer !== options.issuer) throw new OidcIssuerMismatchError();
httpsEndpoint(metadata.authorization_endpoint);
httpsEndpoint(metadata.token_endpoint);
httpsEndpoint(metadata.jwks_uri);
return config;
} catch (error) {
throw protocolFailure(error);
} finally {
inspectingDiscovery = false;
}
})();
}