// 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();