sc-idp/test/adminGate.js
2026-06-01 16:40:54 -05:00

180 lines
7.8 KiB
JavaScript

// Admin defense-in-depth gate (MAIN :3000). Exercises the adminUi.js hardening
// that the role gate sits on top of -- none of which was gate-covered before:
// 1. URL-scheme validation: a javascript:/data: redirect_uri (OIDC client) or
// ACS URL (SAML SP) is rejected (the entity is never created).
// 2. SP signing-cert expiry: an SP registered with an EXPIRED signing cert is
// rejected; a valid http ACS + no cert is accepted (positive control).
// 3. Admin-mutation rate limit: > ADMIN_RATE_MAX admin POSTs in the window
// eventually return HTTP 429 (run LAST -- it throttles the session).
// Run: node idp/test/adminGate.js (MAIN up; logs in as admin@local).
const http = require("http");
const PORT = 3000;
const ADMIN_EMAIL = "admin@local";
const ADMIN_PW = "AdminP@ss1";
const RUN = Date.now();
// A real self-signed cert whose validity window is entirely in 2020 (expired).
const EXPIRED_CERT = [
"-----BEGIN CERTIFICATE-----",
"MIICwDCCAaigAwIBAgIUCTQ9FTQodMj9cA3nEeBD+garqVEwDQYJKoZIhvcNAQEL",
"BQAwGjEYMBYGA1UEAwwPZXhwaXJlZC1zcC10ZXN0MB4XDTIwMDEwMTAwMDAwMFoX",
"DTIwMDEwMjAwMDAwMFowGjEYMBYGA1UEAwwPZXhwaXJlZC1zcC10ZXN0MIIBIjAN",
"BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt4i8DXM2wEj5DEu4wo+i9xD9dydq",
"dmDcqYKu6v8wy6Tm1f704JEJ4gOvsMpBgJfFwT0xxa5MVhvahozdWj8gmQulBsfa",
"mCk8KXWAjHvWc5+gu1nnFVsDU3mUuNWHIRZtdSZkAL9EjjHJ3reZKmQjDWl8Prfe",
"x+jJUk8+CPZLxxAUK6vpERZ2pEOhLe1gjXADGsgRB6OU618hFYb9WodBtm1SaM7c",
"d65HG8jmfM1U3frCBktk00d3Dk0rP/Qz/iWO2lel451H6TpvtQAZq6hjseotLpFa",
"YY2fcFOgPzYg8A8OzrlymjkXmAas11u7XJI8PgUsPvbKyd7WZCtKsLdGOQIDAQAB",
"MA0GCSqGSIb3DQEBCwUAA4IBAQBBHXmy9f38uEJpNVqP4njoYF+NHDF8YHVHpnPF",
"YlLBGFoLa1jaKhrC/CcueHTiZmSxPPQhStosGWhzAFK3aWCSFaj74+T5nxbcHvxj",
"NYjMKUv1f4eczl5GJXOXgYgu1m4t8XI69qesRoj/ZTT1t+q0DdYdvyP/RWwas3Si",
"4qneOomj74OYHf64qrObj9jJ8ii0oTKfLPiQz8Z82fM95gRLq+iUcE4PizN6eBPb",
"URZXCIt2PCvZOI9vr5aFLmcuwSE7QjqNEm7jaMRonhFW0vQ8SprCNmb8meineq6r",
"Lz68ydyzrUeFbIZZwl5Liwaq7Dd3Uk4MCdL2ub/gOzs44CwW",
"-----END CERTIFICATE-----"
].join("\n");
const jar = {};
let pass = 0;
let fail = 0;
const ok = (cond, msg) => {
if (cond) {
pass++;
console.log(" PASS " + msg);
} else {
fail++;
console.log(" FAIL " + msg);
}
};
const storeCookies = (headers) => {
const sc = headers["set-cookie"];
if (!sc) {
return;
}
for (const line of sc) {
const pair = line.split(";")[0];
const eq = pair.indexOf("=");
if (eq > 0) {
jar[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
}
}
};
const request = (method, path, opts) => {
const options = opts || {};
return new Promise((resolve, reject) => {
const headers = Object.assign({}, options.headers || {});
if (Object.keys(jar).length > 0) {
headers["Cookie"] = Object.keys(jar).map((k) => k + "=" + jar[k]).join("; ");
}
let data = null;
if (options.body) {
data = new URLSearchParams(options.body).toString();
headers["Content-Type"] = "application/x-www-form-urlencoded";
headers["Content-Length"] = Buffer.byteLength(data);
}
const r = http.request({ host: "localhost", port: PORT, method: method, path: path, headers: headers }, (resp) => {
storeCookies(resp.headers);
let body = "";
resp.on("data", (c) => { body += c; });
resp.on("end", () => resolve({ status: resp.statusCode, headers: resp.headers, body: body }));
});
r.on("error", reject);
if (data !== null) {
r.write(data);
}
r.end();
});
};
const csrfOf = (html) => (html.match(/name="_csrf" value="([^"]+)"/) || [])[1] || "";
const adminCsrf = async (path) => csrfOf((await request("GET", path)).body);
const run = async () => {
// Login.
const lp = await request("GET", "/auth/login");
await request("POST", "/auth/login", { body: { email: ADMIN_EMAIL, password: ADMIN_PW, _csrf: csrfOf(lp.body) } });
ok(/true/.test((await request("GET", "/auth/authenticated")).body), "admin session authenticated");
// --- 1. URL-scheme validation (OIDC client redirect_uris) ---
const dangerClient = "adminGate_js_" + RUN;
await request("POST", "/admin/idp/clients/create", {
body: { client_id: dangerClient, label: "danger", redirect_uris: "javascript:alert(document.cookie)", auth_method: "none", _csrf: await adminCsrf("/admin/idp/clients") }
});
const clientList1 = (await request("GET", "/admin/idp/clients")).body;
ok(clientList1.indexOf(dangerClient) < 0, "OIDC client with javascript: redirect_uri REJECTED (not created)");
// Positive control: a valid https redirect IS accepted.
const okClient = "adminGate_ok_" + RUN;
await request("POST", "/admin/idp/clients/create", {
body: { client_id: okClient, label: "ok", redirect_uris: "https://example.com/cb", auth_method: "none", _csrf: await adminCsrf("/admin/idp/clients") }
});
const clientList2 = (await request("GET", "/admin/idp/clients")).body;
ok(clientList2.indexOf(okClient) >= 0, "OIDC client with https redirect_uri accepted (control)");
await request("POST", "/admin/idp/clients/delete", { body: { client_id: okClient, _csrf: await adminCsrf("/admin/idp/clients") } });
// --- 1b. URL-scheme validation (SAML ACS) ---
const dangerSp = "http://localhost:9098/adminGateDanger_" + RUN;
await request("POST", "/admin/idp/saml-sps/create", {
body: { entity_id: dangerSp, label: "danger", acs_urls: "data:text/html;base64,PHNjcmlwdD4=", _csrf: await adminCsrf("/admin/idp/saml-sps") }
});
const spList1 = (await request("GET", "/admin/idp/saml-sps")).body;
ok(spList1.indexOf(dangerSp) < 0, "SAML SP with data: ACS URL REJECTED (not created)");
// --- 2. SP signing-cert expiry ---
const expiredSp = "http://localhost:9098/adminGateExpired_" + RUN;
await request("POST", "/admin/idp/saml-sps/create", {
body: { entity_id: expiredSp, label: "expired", acs_urls: "https://example.com/acs", signing_cert: EXPIRED_CERT, _csrf: await adminCsrf("/admin/idp/saml-sps") }
});
const spList2 = (await request("GET", "/admin/idp/saml-sps")).body;
ok(spList2.indexOf(expiredSp) < 0, "SAML SP with EXPIRED signing cert REJECTED (not created)");
// Positive control: valid http ACS, no cert, IS accepted.
const okSp = "http://localhost:9098/adminGateOk_" + RUN;
await request("POST", "/admin/idp/saml-sps/create", {
body: { entity_id: okSp, label: "ok", acs_urls: "https://example.com/acs", _csrf: await adminCsrf("/admin/idp/saml-sps") }
});
const spList3 = (await request("GET", "/admin/idp/saml-sps")).body;
ok(spList3.indexOf(okSp) >= 0, "SAML SP with valid ACS + no cert accepted (control)");
await request("POST", "/admin/idp/saml-sps/delete", { body: { entity_id: okSp, _csrf: await adminCsrf("/admin/idp/saml-sps") } });
// --- 3. Admin-mutation rate limit (LAST -- throttles the session) ---
// Reuse one CSRF token across a burst of harmless no-op deletes; requireAdmin
// throttles every admin POST, so we should hit HTTP 429 within the window.
const csrf = await adminCsrf("/admin/idp/saml-sps");
let saw429 = false;
let count = 0;
for (let i = 0; i < 260 && !saw429; i++) {
const r = await request("POST", "/admin/idp/saml-sps/delete", { body: { entity_id: "__adminGate_probe__", _csrf: csrf } });
count++;
if (r.status === 429) {
saw429 = true;
}
}
ok(saw429, "admin-mutation rate limit returned 429 after " + count + " POSTs");
console.log("\nRESULT: " + pass + " passed, " + fail + " failed");
process.exit(fail ? 1 : 0);
};
const main = async () => {
try {
await run();
} catch (e) {
console.error("ADMIN GATE ERROR:", e);
process.exit(2);
}
};
main();