#!/usr/bin/env node /** * Hermetic loopback OIDC/AuthentiK-shaped provider for browser smoke tests. * * It deliberately has no network dependency and binds only to 127.0.0.1. Its * ephemeral TLS and signing keys are test-scoped; callers receive the CA path * needed to trust the provider from a spawned backend process. */ import { spawnSync } from "node:child_process"; import { createHash, generateKeyPairSync, randomBytes, sign as signRsa, timingSafeEqual, } from "node:crypto"; import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { createServer } from "node:https"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const LOOPBACK_HOST = "127.0.0.1"; const ISSUER_PATH = "/application/o/thothii"; const MAX_BODY_BYTES = 32 * 1024; const MAX_STATE_TTL_MS = 5 * 60 * 1_000; const MAX_STATE_LIMIT = 1_024; const MAX_DEVICE_POLLS = 32; const VALID_IDENTITIES = new Set([ "ordinary", "admin", "missing-groups", "malformed-groups", "unmapped", "expired", ]); const identityClaims = Object.freeze({ ordinary: { subject: "fixture-ordinary", displayName: "Fixture ordinary", groups: ["fixture-users"] }, admin: { subject: "fixture-admin", displayName: "Fixture administrator", groups: ["fixture-admin"] }, "missing-groups": { subject: "fixture-missing-groups", displayName: "Fixture missing groups" }, "malformed-groups": { subject: "fixture-malformed-groups", displayName: "Fixture malformed groups", groups: ["fixture-users", 7] }, unmapped: { subject: "fixture-unmapped", displayName: "Fixture unmapped", groups: ["fixture-unmapped"] }, expired: { subject: "fixture-expired", displayName: "Fixture expired", groups: ["fixture-users"], expired: true }, }); function safeError(code) { return new Error(code); } function boundedInteger(value, fallback, maximum, code) { const selected = value ?? fallback; if (!Number.isSafeInteger(selected) || selected < 1 || selected > maximum) throw safeError(code); return selected; } function boundedNonNegativeInteger(value, fallback, maximum, code) { const selected = value ?? fallback; if (!Number.isSafeInteger(selected) || selected < 0 || selected > maximum) throw safeError(code); return selected; } function controlledString(value, code) { if (typeof value !== "string" || value.length < 1 || value.length > 512 || /[\r\n]/u.test(value)) { throw safeError(code); } return value; } function exactParameter(values, name) { const matches = values.getAll(name); return matches.length === 1 && matches[0].length > 0 ? matches[0] : undefined; } function secretMatches(actual, expected) { if (typeof actual !== "string") return false; const actualDigest = createHash("sha256").update(actual).digest(); const expectedDigest = createHash("sha256").update(expected).digest(); return timingSafeEqual(actualDigest, expectedDigest); } function pruneExpired(records, currentTime) { for (const [key, record] of records) { if (record.expiresAt <= currentTime) records.delete(key); } } function ensurePrivateDirectory(directory) { mkdirSync(directory, { recursive: true, mode: 0o700 }); chmodSync(directory, 0o700); } function createCertificate(directory) { const keyFile = join(directory, "provider-key.pem"); const certificateFile = join(directory, "provider-ca.pem"); const result = spawnSync("openssl", [ "req", "-x509", "-newkey", "rsa:2048", "-sha256", "-nodes", "-keyout", keyFile, "-out", certificateFile, "-subj", "/CN=127.0.0.1", "-addext", "subjectAltName=IP:127.0.0.1", "-days", "1", ], { stdio: "ignore" }); if (result.status !== 0) throw safeError("oidc_fixture_certificate_generation_failed"); chmodSync(keyFile, 0o600); chmodSync(certificateFile, 0o600); return { key: readFileSync(keyFile), cert: readFileSync(certificateFile), certificateFile }; } function sendJson(reply, status, value) { reply.writeHead(status, { "cache-control": "no-store", "content-type": "application/json; charset=utf-8", }); reply.end(JSON.stringify(value)); } function redirect(reply, location) { reply.writeHead(302, { "cache-control": "no-store", location }); reply.end(); } async function requestBody(request) { let size = 0; const chunks = []; for await (const chunk of request) { size += chunk.length; if (size > MAX_BODY_BYTES) throw safeError("oidc_fixture_request_too_large"); chunks.push(chunk); } return Buffer.concat(chunks).toString("utf8"); } function jwt(privateKey, issuer, audience, nonce, identity, currentTime) { const claims = identityClaims[identity]; const now = Math.floor(currentTime / 1_000); const payload = { iss: issuer, sub: claims.subject, aud: audience, exp: now + (claims.expired ? -30 : 60), iat: now - 1, nonce, name: claims.displayName, ...(Object.hasOwn(claims, "groups") ? { groups: claims.groups } : {}), }; const header = { alg: "RS256", typ: "JWT", kid: "fixture-rs256" }; const protectedPart = Buffer.from(JSON.stringify(header)).toString("base64url"); const payloadPart = Buffer.from(JSON.stringify(payload)).toString("base64url"); const signingInput = `${protectedPart}.${payloadPart}`; const signature = signRsa("RSA-SHA256", Buffer.from(signingInput), privateKey).toString("base64url"); return `${signingInput}.${signature}`; } function authorizationIdentity(identity) { if (!VALID_IDENTITIES.has(identity)) throw safeError("oidc_fixture_identity_invalid"); return identity; } /** * Start an HTTPS test provider. The returned telemetry intentionally excludes * transient authorization codes, browser state, nonces, and token material. */ export async function startFakeOidcProvider(options = {}) { const host = options.host ?? LOOPBACK_HOST; if (host !== LOOPBACK_HOST) throw safeError("oidc_fixture_loopback_required"); const registration = options.registration; if (!registration || typeof registration !== "object") throw safeError("oidc_fixture_registration_required"); const clientId = controlledString(registration.clientId, "oidc_fixture_client_id_invalid"); const clientSecret = controlledString(registration.clientSecret, "oidc_fixture_client_secret_invalid"); const redirectUri = controlledString(registration.redirectUri, "oidc_fixture_redirect_invalid"); let parsedRedirect; try { parsedRedirect = new URL(redirectUri); } catch { throw safeError("oidc_fixture_redirect_invalid"); } if (parsedRedirect.protocol !== "http:" || parsedRedirect.hostname !== LOOPBACK_HOST || parsedRedirect.username || parsedRedirect.password || parsedRedirect.hash) { throw safeError("oidc_fixture_redirect_invalid"); } const apiToken = controlledString(options.apiToken, "oidc_fixture_api_token_required"); const now = options.now ?? Date.now; if (typeof now !== "function") throw safeError("oidc_fixture_clock_invalid"); const authorizationStateTtlMs = boundedInteger( options.authorizationStateTtlMs, 60_000, MAX_STATE_TTL_MS, "oidc_fixture_authorization_ttl_invalid", ); const authorizationStateLimit = boundedInteger( options.authorizationStateLimit, 64, MAX_STATE_LIMIT, "oidc_fixture_authorization_limit_invalid", ); const deviceStateTtlMs = boundedInteger( options.deviceStateTtlMs, 60_000, MAX_STATE_TTL_MS, "oidc_fixture_device_ttl_invalid", ); const deviceStateLimit = boundedInteger( options.deviceStateLimit, 32, MAX_STATE_LIMIT, "oidc_fixture_device_limit_invalid", ); const devicePendingPolls = boundedNonNegativeInteger( options.devicePendingPolls, 0, MAX_DEVICE_POLLS, "oidc_fixture_device_pending_polls_invalid", ); const devicePollLimit = boundedInteger( options.devicePollLimit, 5, MAX_DEVICE_POLLS, "oidc_fixture_device_poll_limit_invalid", ); const ownsDirectory = options.directory === undefined; const directory = options.directory ?? mkdtempSync(join(tmpdir(), "thothii-oidc-fixture-")); ensurePrivateDirectory(directory); const certificate = createCertificate(directory); const { privateKey, publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); const publicJwk = publicKey.export({ format: "jwk" }); const jwks = { keys: [{ ...publicJwk, alg: "RS256", kid: "fixture-rs256", use: "sig" }], }; const authorizations = new Map(); const deviceCodes = new Map(); let activeIdentity = authorizationIdentity(options.identity ?? "ordinary"); let lastAuthorization = undefined; let issuer = undefined; let baseUrl = undefined; function currentTime() { const value = now(); if (!Number.isSafeInteger(value) || value < 0) throw safeError("oidc_fixture_clock_invalid"); return value; } function authenticateClient(request, values) { if (request.headers.authorization !== undefined) return false; const suppliedClientId = exactParameter(values, "client_id"); const suppliedClientSecret = exactParameter(values, "client_secret"); return secretMatches(suppliedClientId, clientId) && secretMatches(suppliedClientSecret, clientSecret); } const server = createServer({ key: certificate.key, cert: certificate.cert }, async (request, reply) => { try { if (!issuer || !baseUrl || !request.url) { sendJson(reply, 503, { error: "temporarily_unavailable" }); return; } const url = new URL(request.url, baseUrl); const path = url.pathname; const discoveryPath = `${ISSUER_PATH}/.well-known/openid-configuration`; const rfc8414Path = `/.well-known/openid-configuration${ISSUER_PATH}`; if (request.method === "GET" && (path === discoveryPath || path === rfc8414Path)) { sendJson(reply, 200, { issuer, authorization_endpoint: `${issuer}authorize`, token_endpoint: `${issuer}token`, jwks_uri: `${issuer}jwks`, device_authorization_endpoint: `${issuer}device_authorization`, response_types_supported: ["code"], subject_types_supported: ["public"], grant_types_supported: ["authorization_code", "urn:ietf:params:oauth:grant-type:device_code"], token_endpoint_auth_methods_supported: ["client_secret_post"], code_challenge_methods_supported: ["S256"], id_token_signing_alg_values_supported: ["RS256"], }); return; } if (request.method === "GET" && path === `${ISSUER_PATH}/jwks`) { sendJson(reply, 200, jwks); return; } if (request.method === "GET" && path === `${ISSUER_PATH}/authorize`) { const suppliedRedirectUri = exactParameter(url.searchParams, "redirect_uri"); const suppliedClientId = exactParameter(url.searchParams, "client_id"); const state = exactParameter(url.searchParams, "state"); const nonce = exactParameter(url.searchParams, "nonce"); const challenge = exactParameter(url.searchParams, "code_challenge"); if (exactParameter(url.searchParams, "response_type") !== "code" || !secretMatches(suppliedRedirectUri, redirectUri) || !secretMatches(suppliedClientId, clientId) || !state || !nonce || !challenge || exactParameter(url.searchParams, "code_challenge_method") !== "S256") { sendJson(reply, 400, { error: "invalid_request" }); return; } const reservationTime = currentTime(); pruneExpired(authorizations, reservationTime); if (authorizations.size >= authorizationStateLimit) { sendJson(reply, 503, { error: "temporarily_unavailable" }); return; } const callback = new URL(redirectUri); const code = randomBytes(32).toString("base64url"); authorizations.set(code, { challenge, clientId, redirectUri, identity: activeIdentity, nonce, expiresAt: reservationTime + authorizationStateTtlMs, }); lastAuthorization = { identity: activeIdentity, codeChallengeMethod: "S256", pkceVerified: false }; callback.searchParams.set("code", code); callback.searchParams.set("state", state); redirect(reply, callback.href); return; } if (request.method === "POST" && path === `${ISSUER_PATH}/device_authorization`) { const values = new URLSearchParams(await requestBody(request)); if (!authenticateClient(request, values)) { sendJson(reply, 401, { error: "invalid_client" }); return; } const reservationTime = currentTime(); pruneExpired(deviceCodes, reservationTime); if (deviceCodes.size >= deviceStateLimit) { sendJson(reply, 503, { error: "temporarily_unavailable" }); return; } const deviceCode = randomBytes(32).toString("base64url"); const userCode = "FIXTURE-CODE"; deviceCodes.set(deviceCode, { clientId, identity: activeIdentity, nonce: "device", expiresAt: reservationTime + deviceStateTtlMs, polls: 0, }); sendJson(reply, 200, { device_code: deviceCode, user_code: userCode, verification_uri: `${issuer}device`, verification_uri_complete: `${issuer}device?user_code=${userCode}`, expires_in: Math.max(1, Math.floor(deviceStateTtlMs / 1_000)), interval: 1, }); return; } if (request.method === "POST" && path === `${ISSUER_PATH}/token`) { const values = new URLSearchParams(await requestBody(request)); if (!authenticateClient(request, values)) { sendJson(reply, 401, { error: "invalid_client" }); return; } const tokenTime = currentTime(); pruneExpired(authorizations, tokenTime); pruneExpired(deviceCodes, tokenTime); const grantType = exactParameter(values, "grant_type"); let record; if (grantType === "authorization_code") { const code = exactParameter(values, "code") ?? ""; record = authorizations.get(code); if (record !== undefined) authorizations.delete(code); const verifier = exactParameter(values, "code_verifier") ?? ""; const suppliedRedirectUri = exactParameter(values, "redirect_uri"); const suppliedClientId = exactParameter(values, "client_id"); const verified = record !== undefined && secretMatches(suppliedClientId, record.clientId) && secretMatches(suppliedRedirectUri, record.redirectUri) && secretMatches(createHash("sha256").update(verifier).digest("base64url"), record.challenge); if (!verified) { sendJson(reply, 400, { error: "invalid_grant" }); return; } if (lastAuthorization) lastAuthorization = { ...lastAuthorization, pkceVerified: true }; } else if (grantType === "urn:ietf:params:oauth:grant-type:device_code") { const deviceCode = exactParameter(values, "device_code") ?? ""; record = deviceCodes.get(deviceCode); if (record === undefined) { sendJson(reply, 400, { error: "invalid_grant" }); return; } record.polls += 1; if (record.polls >= devicePollLimit && record.polls <= devicePendingPolls) { deviceCodes.delete(deviceCode); sendJson(reply, 400, { error: "expired_token" }); return; } if (record.polls <= devicePendingPolls) { sendJson(reply, 400, { error: "authorization_pending" }); return; } deviceCodes.delete(deviceCode); } else { sendJson(reply, 400, { error: "unsupported_grant_type" }); return; } sendJson(reply, 200, { access_token: randomBytes(32).toString("base64url"), token_type: "Bearer", expires_in: 60, id_token: jwt(privateKey, issuer, record.clientId, record.nonce, record.identity, tokenTime), }); return; } if (request.method === "GET" && path === "/api/v3/core/groups/") { const authorization = request.headers.authorization; if (!secretMatches(authorization, `Bearer ${apiToken}`)) { sendJson(reply, 401, { detail: "authentication required" }); return; } const name = url.searchParams.get("name") ?? ""; const present = name === "fixture-users" || name === "fixture-admin"; sendJson(reply, 200, { pagination: { next: null }, results: present ? [{ name }] : [], }); return; } sendJson(reply, 404, { error: "not_found" }); } catch { if (!reply.headersSent) sendJson(reply, 400, { error: "invalid_request" }); else reply.end(); } }); try { await new Promise((resolveListen, rejectListen) => { const onError = (error) => rejectListen(error); server.once("error", onError); server.listen({ host, port: options.port ?? 0 }, () => { server.off("error", onError); resolveListen(); }); }); } catch (error) { server.close(); if (ownsDirectory) rmSync(directory, { recursive: true, force: true }); throw error; } const address = server.address(); if (!address || typeof address === "string") { await new Promise((resolveClose) => server.close(resolveClose)); if (ownsDirectory) rmSync(directory, { recursive: true, force: true }); throw safeError("oidc_fixture_listen_failed"); } baseUrl = `https://${LOOPBACK_HOST}:${address.port}`; issuer = `${baseUrl}${ISSUER_PATH}/`; return { baseUrl, issuer, caFile: certificate.certificateFile, setIdentity(identity) { activeIdentity = authorizationIdentity(identity); }, lastAuthorization() { return lastAuthorization === undefined ? undefined : { ...lastAuthorization }; }, async close() { await new Promise((resolveClose) => server.close(resolveClose)); if (ownsDirectory) rmSync(directory, { recursive: true, force: true }); }, }; } const currentFile = fileURLToPath(import.meta.url); if (process.argv[1] && resolve(process.argv[1]) === currentFile) { const provider = await startFakeOidcProvider({ registration: { clientId: "fixture-standalone-client", clientSecret: "fixture-standalone-secret-not-production", redirectUri: "http://127.0.0.1:8787/api/auth/oidc/callback", }, apiToken: "fixture-standalone-api-token-not-production", }); process.stdout.write('{"status":"ready"}\n'); const close = async () => { await provider.close(); process.exit(0); }; process.once("SIGINT", () => { void close(); }); process.once("SIGTERM", () => { void close(); }); }