// What holds keycloak to the `oidc-client` provision (oidc.ts): one confidential client per consumer, // under the id and secret the mesh gave, redirecting only to the consumer's own callback under the // names the mesh composed; made once and brought back on every apply; and a client the mesh did not // make — same id or not — never adopted, changed or deleted. // // Keycloak is a fake: the admin routes the module touches, answering with the status codes and the // shapes Keycloak gives. Run against the compiled module (npm test builds first), the way the runtime // loads it. import { test, after } from "node:test"; import assert from "node:assert/strict"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { randomUUID } from "node:crypto"; import { KeycloakClient } from "../dist/client.js"; import { MARK, OidcClients, ROLES_MAPPER, realmOf, redirectsOf } from "../dist/oidc.js"; type Client = Record; /** The realm's clients, by internal id, and what the fake was asked. */ const realm = "Novox"; const clients = new Map(); const calls: string[] = []; function body(req: IncomingMessage): Promise { return new Promise((resolve) => { let raw = ""; req.on("data", (c) => (raw += c)); req.on("end", () => resolve(raw ? JSON.parse(raw) : undefined)); }); } function send(res: ServerResponse, status: number, value?: unknown): void { res.writeHead(status, { "Content-Type": "application/json" }); res.end(value === undefined ? "" : JSON.stringify(value)); } const server = createServer(async (req, res) => { const url = new URL(req.url!, "http://fake"); calls.push(`${req.method} ${url.pathname}`); if (url.pathname === "/realms/master/protocol/openid-connect/token") { return send(res, 200, { access_token: "t", expires_in: 300 }); } const base = `/admin/realms/${realm}/clients`; if (!url.pathname.startsWith(base)) return send(res, 404, { error: "Realm not found." }); const rest = url.pathname.slice(base.length).split("/").filter(Boolean); if (rest.length === 0 && req.method === "GET") { const want = url.searchParams.get("clientId"); return send(res, 200, [...clients.values()].filter((c) => !want || c.clientId === want)); } if (rest.length === 0 && req.method === "POST") { const rep = await body(req); if ([...clients.values()].some((c) => c.clientId === rep.clientId)) { return send(res, 409, { errorMessage: `Client ${rep.clientId} already exists` }); } const id = randomUUID(); const mappers = (rep.protocolMappers ?? []).map((m: Client) => ({ ...m, id: randomUUID() })); clients.set(id, { ...rep, id, protocolMappers: mappers }); return send(res, 201); } const c = clients.get(rest[0]); if (!c) return send(res, 404, { error: "Could not find client" }); if (rest.length === 1 && req.method === "PUT") { // Keycloak ignores protocolMappers on a client update: they have their own endpoints. const rep = await body(req); clients.set(c.id, { ...rep, id: c.id, protocolMappers: c.protocolMappers }); return send(res, 204); } if (rest.length === 1 && req.method === "DELETE") { clients.delete(c.id); return send(res, 204); } if (rest[1] === "client-secret" && req.method === "GET") { return send(res, 200, { type: "secret", value: c.secret }); } if (rest[1] === "protocol-mappers") { if (req.method === "GET") return send(res, 200, c.protocolMappers ?? []); if (req.method === "POST") { c.protocolMappers = [...(c.protocolMappers ?? []), { ...(await body(req)), id: randomUUID() }]; return send(res, 201); } if (req.method === "PUT") { const m = await body(req); c.protocolMappers = c.protocolMappers.map((x: Client) => (x.id === rest[4] ? m : x)); return send(res, 204); } } send(res, 405); }); await new Promise((r) => server.listen(0, "127.0.0.1", r)); after(() => server.close()); const port = (server.address() as { port: number }).port; const oidc = new OidcClients(new KeycloakClient(`http://127.0.0.1:${port}`, "admin", "pw"), realm); /** Grafana on ace, as the mesh hands it to the provisioner. */ function grafana(secret = "s3cret", values: Record = {}) { return { as: "mesh_ace_grafana", password: secret, consumer: "ace", values: { label: "grafana", endpoint: "web", port: 20010, callback: "/login/generic_oauth", name: "grafana.zurag.be", "internal-name": "grafana.ace.internal", ...values, }, }; } function only(clientId: string): Client { const found = [...clients.values()].filter((c) => c.clientId === clientId); assert.equal(found.length, 1, `exactly one client ${clientId}, found ${found.length}`); return found[0]; } test("the realm is read out of the issuer, and an issuer that names none is refused", () => { assert.equal(realmOf("https://keycloak.novox.be/realms/Novox"), "Novox"); assert.equal(realmOf("https://keycloak.novox.be/realms/Novox/"), "Novox"); assert.equal(realmOf("http://127.0.0.1:18500/realms/master"), "master"); assert.throws(() => realmOf("https://keycloak.novox.be"), /realms/); assert.throws(() => realmOf("keycloak"), /not a URL/); }); test("the redirect is the consumer's callback under every name the mesh composed for it", () => { assert.deepEqual(redirectsOf(grafana().values), { root: "https://grafana.zurag.be", redirects: ["https://grafana.zurag.be/login/generic_oauth", "https://grafana.ace.internal/login/generic_oauth"], }); // A route reaching only the private network has only the internal name, and that is enough. assert.deepEqual(redirectsOf({ callback: "/cb", "internal-name": "x.ace.internal" }).redirects, ["https://x.ace.internal/cb"]); assert.throws(() => redirectsOf({ name: "grafana.zurag.be" }), /callback/); assert.throws(() => redirectsOf({ name: "grafana.zurag.be", callback: "login" }), /callback/); assert.throws(() => redirectsOf({ callback: "/cb" }), /label/); }); test("a consumer is given one confidential client, under its id and the mesh's secret", async () => { clients.clear(); assert.equal(await oidc.ensure(grafana()), "created"); const c = only("mesh_ace_grafana"); assert.equal(c.publicClient, false); assert.equal(c.clientAuthenticatorType, "client-secret"); assert.equal(c.secret, "s3cret"); assert.equal(c.enabled, true); assert.equal(c.standardFlowEnabled, true); assert.equal(c.directAccessGrantsEnabled, false); assert.equal(c.implicitFlowEnabled, false); assert.deepEqual(c.redirectUris, [ "https://grafana.zurag.be/login/generic_oauth", "https://grafana.ace.internal/login/generic_oauth"]); assert.equal(c.attributes[MARK], "true"); assert.deepEqual(c.protocolMappers.map((m: Client) => m.name), [ROLES_MAPPER.name]); assert.equal(await oidc.holds(grafana()), true); }); test("applying the same grant again makes no second client", async () => { clients.clear(); await oidc.ensure(grafana()); assert.equal(await oidc.ensure(grafana()), "updated"); assert.equal(await oidc.ensure(grafana()), "updated"); only("mesh_ace_grafana"); assert.equal(only("mesh_ace_grafana").protocolMappers.length, 1, "the roles mapper is not added twice"); }); test("a new secret or a moved name is applied in place, and what the mesh does not own survives", async () => { clients.clear(); await oidc.ensure(grafana()); const id = only("mesh_ace_grafana").id; // Something the mesh does not own, set on the client after it was made. clients.get(id)!.consentRequired = true; clients.get(id)!.attributes["post.logout.redirect.uris"] = "+"; assert.equal(await oidc.holds(grafana("rotated")), false, "a rotated secret is not held until applied"); await oidc.ensure(grafana("rotated", { name: "dash.zurag.be" })); const c = only("mesh_ace_grafana"); assert.equal(c.id, id, "updated, not replaced"); assert.equal(c.secret, "rotated"); assert.deepEqual(c.redirectUris, [ "https://dash.zurag.be/login/generic_oauth", "https://grafana.ace.internal/login/generic_oauth"]); assert.equal(c.rootUrl, "https://dash.zurag.be"); assert.equal(c.consentRequired, true); assert.equal(c.attributes["post.logout.redirect.uris"], "+"); assert.equal(c.attributes[MARK], "true"); assert.equal(await oidc.holds(grafana("rotated", { name: "dash.zurag.be" })), true); }); test("a client lost or edited behind the mesh's back is not held, and is made whole again", async () => { clients.clear(); await oidc.ensure(grafana()); const c = only("mesh_ace_grafana"); c.redirectUris = ["*"]; assert.equal(await oidc.holds(grafana()), false, "a widened redirect is not what the mesh gave"); await oidc.ensure(grafana()); assert.equal(await oidc.holds(grafana()), true); only("mesh_ace_grafana").protocolMappers = []; assert.equal(await oidc.holds(grafana()), false, "a client without its roles mapper is not held"); await oidc.ensure(grafana()); assert.equal(await oidc.holds(grafana()), true); clients.clear(); assert.equal(await oidc.holds(grafana()), false); }); test("a client of the same id the mesh did not make is refused, and left exactly as it was", async () => { clients.clear(); clients.set("theirs", { id: "theirs", clientId: "mesh_ace_grafana", secret: "their-secret", redirectUris: ["*"] }); const before = JSON.stringify(clients.get("theirs")); const writes = calls.length; await assert.rejects(oidc.ensure(grafana()), /did not make/); assert.equal(JSON.stringify(clients.get("theirs")), before); assert.ok(calls.slice(writes).every((c) => c.startsWith("GET") || c.startsWith("POST /realms/master")), `only reads were made: ${calls.slice(writes).join(", ")}`); assert.equal(await oidc.holds(grafana()), false); assert.equal(await oidc.remove("mesh_ace_grafana"), "not ours"); assert.ok(clients.has("theirs"), "a client the mesh did not make is never deleted"); }); test("the predecessor's hand-made client is never touched: the mesh's has its own id", async () => { clients.clear(); clients.set("hal", { id: "hal", clientId: "grafana", secret: "old", redirectUris: ["https://grafana.zurag.be/*"] }); await oidc.ensure(grafana()); assert.equal(clients.get("hal")!.secret, "old"); only("mesh_ace_grafana"); assert.equal(await oidc.remove("grafana"), "not ours"); assert.ok(clients.has("hal")); }); test("a withdrawn consumer's client is removed, and an absent one is not an error", async () => { clients.clear(); await oidc.ensure(grafana()); assert.equal(await oidc.remove("mesh_ace_grafana"), "removed"); assert.equal([...clients.values()].length, 0); assert.equal(await oidc.remove("mesh_ace_grafana"), "absent"); }); test("a contribution with no callback makes no client at all", async () => { clients.clear(); await assert.rejects(oidc.ensure({ ...grafana(), values: { name: "grafana.zurag.be" } }), /callback/); assert.equal(clients.size, 0); });