fix(auth): make diagnostics bounded and portable
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
} from "openid-client";
|
||||
import { constants, createPublicKey, verify as verifySignature } from "node:crypto";
|
||||
import { parseConfiguredTransportUrl } from "./url-policy.js";
|
||||
import { isUsableAuthenticationSecret } from "./secret-policy.js";
|
||||
|
||||
export interface OidcIdentity {
|
||||
issuer: string;
|
||||
@@ -79,6 +80,23 @@ const MAX_JWKS_TIMEOUT_MS = 30_000;
|
||||
const text = (value: unknown, maximum = 2048): value is string =>
|
||||
typeof value === "string" && value.length > 0 && value.length <= maximum && !/\p{Cc}/u.test(value);
|
||||
|
||||
function discoveryStringList(value: unknown): value is readonly string[] {
|
||||
return Array.isArray(value) && value.length > 0 && value.length <= 128
|
||||
&& value.every((item) => text(item, 128));
|
||||
}
|
||||
|
||||
function schemaValidDiscoveryMetadata(value: unknown): value is Record<string, unknown> & { issuer: string } {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const metadata = value as Record<string, unknown>;
|
||||
return text(metadata.issuer, 2048)
|
||||
&& text(metadata.authorization_endpoint, 2048)
|
||||
&& text(metadata.token_endpoint, 2048)
|
||||
&& text(metadata.jwks_uri, 2048)
|
||||
&& discoveryStringList(metadata.response_types_supported)
|
||||
&& discoveryStringList(metadata.subject_types_supported)
|
||||
&& discoveryStringList(metadata.id_token_signing_alg_values_supported);
|
||||
}
|
||||
|
||||
function configuredHttpsUrl(value: string): URL {
|
||||
const url = parseConfiguredTransportUrl(value, { allowLoopbackHttp: false });
|
||||
if (!url) throw new OidcProtocolError();
|
||||
@@ -463,7 +481,7 @@ async function verifyJwksAvailability(
|
||||
export function createOidcProtocol(options: OidcProtocolOptions): OidcProtocol {
|
||||
const issuerUrl = configuredHttpsUrl(options.issuer);
|
||||
const callbackUrl = configuredCallbackUrl(options.callbackUrl);
|
||||
if (!text(options.clientId, 512) || !text(options.clientSecret, 4096)
|
||||
if (!text(options.clientId, 512) || !isUsableAuthenticationSecret("THT_OIDC_CLIENT_SECRET", options.clientSecret)
|
||||
|| !text(options.groupsClaim, 128) || options.scopes.length === 0 || options.scopes.length > 16
|
||||
|| options.scopes.some((scope) => !text(scope, 128))
|
||||
|| (options.httpTimeoutMs !== undefined && (!Number.isSafeInteger(options.httpTimeoutMs)
|
||||
@@ -478,19 +496,24 @@ export function createOidcProtocol(options: OidcProtocolOptions): OidcProtocol {
|
||||
const configuration = async (): Promise<Configuration> => {
|
||||
if (!discovered) {
|
||||
discovered = (async () => {
|
||||
let inspectingDiscovery = true;
|
||||
let certifiedIssuerMismatch = false;
|
||||
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.
|
||||
if (!response.ok) return response;
|
||||
try {
|
||||
const metadata: unknown = await response.clone().json();
|
||||
if (schemaValidDiscoveryMetadata(metadata) && metadata.issuer !== options.issuer) {
|
||||
certifiedIssuerMismatch = true;
|
||||
const headers = new Headers(response.headers);
|
||||
headers.delete("content-length");
|
||||
return new Response(JSON.stringify({ ...metadata, issuer: options.issuer }), {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// The OIDC library owns malformed discovery-document classification.
|
||||
}
|
||||
return response;
|
||||
};
|
||||
@@ -503,15 +526,14 @@ export function createOidcProtocol(options: OidcProtocolOptions): OidcProtocol {
|
||||
{ [customFetch]: issuerCheckingFetch, timeout: httpTimeoutMs / 1000 },
|
||||
);
|
||||
const metadata = config.serverMetadata();
|
||||
if (metadata.issuer !== options.issuer) throw new OidcIssuerMismatchError();
|
||||
if (certifiedIssuerMismatch) throw new OidcIssuerMismatchError();
|
||||
if (metadata.issuer !== options.issuer) throw new OidcProtocolError();
|
||||
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