Files
keycloak-pattern/e2e/pattern4.mjs
T

139 lines
4.7 KiB
JavaScript

import assert from "node:assert/strict";
import { chromium } from "playwright-core";
const password = process.env.E2E_PASSWORD;
assert.ok(password, "E2E_PASSWORD must be set");
const edgeBaseUrl = "http://localhost:8088";
const edgeEntryUrl = `${edgeBaseUrl}/`;
async function completeKeycloakLogin(page) {
for (let attempt = 1; attempt <= 2; attempt += 1) {
await page.locator("#username").fill(
process.env.E2E_USERNAME ?? "regular-user",
);
await page.locator("#password").fill(password);
await page.locator("#kc-login").click();
await page.waitForLoadState("domcontentloaded");
if (page.url() === edgeEntryUrl) {
return;
}
if (attempt === 1) {
await page.goto(`${edgeBaseUrl}/oauth2/start?rd=${encodeURIComponent(edgeEntryUrl)}`);
await page.waitForURL(/localhost:8080/u);
}
}
throw new Error(`Keycloak login did not return to AP4: ${page.url()}`);
}
const browser = await chromium.launch({
executablePath: process.env.CHROME_BIN ?? "/usr/bin/google-chrome",
headless: true,
args: ["--no-sandbox"],
});
try {
const context = await browser.newContext();
const page = await context.newPage();
const browserRequests = [];
page.on("request", (request) =>
browserRequests.push({ method: request.method(), url: request.url() }),
);
const edgeResponsePromise = page.waitForResponse(
(response) =>
response.url() === edgeEntryUrl &&
response.status() === 302,
);
const authorizationRequestPromise = page.waitForRequest((request) =>
request.url().includes(
"/protocol/openid-connect/auth?approval_prompt=",
),
);
await page.goto(edgeEntryUrl);
const unauthenticatedEdgeResponse = await edgeResponsePromise;
assert.equal(unauthenticatedEdgeResponse.status(), 302);
const authorizationRequest = await authorizationRequestPromise;
const authorizationUrl = new URL(authorizationRequest.url());
assert.equal(authorizationUrl.searchParams.get("client_id"), "edge-proxy");
assert.equal(authorizationUrl.searchParams.get("code_challenge_method"), "S256");
assert.ok(authorizationUrl.searchParams.get("code_challenge"));
await page.waitForURL(/localhost:8080/u);
await completeKeycloakLogin(page);
const edgeIdentity = JSON.parse(await page.locator("body").innerText());
assert.equal(edgeIdentity.pattern, "AP4-edge-forward-auth");
assert.ok(edgeIdentity.user);
assert.equal(edgeIdentity.identityHeader, "X-Auth-Request-User");
const callbackRequest = browserRequests.find(({ url }) =>
url.startsWith(`${edgeBaseUrl}/oauth2/callback?`),
);
assert.ok(callbackRequest);
assert.equal(callbackRequest.method, "GET");
assert.equal(
browserRequests.some(({ url }) =>
url.includes("/protocol/openid-connect/token"),
),
false,
"the confidential token exchange must be server-to-server",
);
const cookies = await context.cookies(edgeEntryUrl);
const sessionCookie = cookies.find((cookie) => cookie.name === "AP4_SESSION");
assert.ok(sessionCookie);
assert.equal(sessionCookie.httpOnly, true);
assert.equal(sessionCookie.sameSite, "Lax");
assert.equal(sessionCookie.secure, false);
const storage = await page.evaluate(() => ({
localStorage: Object.values(localStorage),
sessionStorage: Object.values(sessionStorage),
readableCookies: document.cookie,
}));
assert.deepEqual(storage.localStorage, []);
assert.deepEqual(storage.sessionStorage, []);
assert.equal(storage.readableCookies.includes("AP4_SESSION"), false);
const spoofAttempt = await page.evaluate(async () => {
const response = await fetch("/", {
headers: {
"X-Auth-Request-User": "spoofed-admin",
"X-Auth-Request-Email": "spoofed-admin@example.test",
"X-Internal-Auth-Token": "attacker-controlled-token",
},
});
return { status: response.status, body: await response.json() };
});
assert.equal(spoofAttempt.status, 200);
assert.equal(spoofAttempt.body.user, edgeIdentity.user);
assert.notEqual(spoofAttempt.body.user, "spoofed-admin");
const externalAuthSubrequest = await fetch(`${edgeBaseUrl}/oauth2/auth`);
assert.equal(externalAuthSubrequest.status, 404);
const apiResponse = await fetch(`${edgeBaseUrl}/api/edge`, {
redirect: "manual",
});
assert.equal(apiResponse.status, 401);
assert.equal(apiResponse.headers.get("location"), null);
await assert.rejects(
fetch("http://localhost:4180/ping"),
"oauth2-proxy must not be published on the host",
);
await assert.rejects(
fetch("http://localhost:8081/edge/me"),
"backend must not be published on the host",
);
console.log(
"pattern4 hardened edge verified: auth_request, no backend publish, spoofed headers overwritten",
);
} finally {
await browser.close();
}