fix(auth): make diagnostics match runtime safety
This commit is contained in:
@@ -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;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user