import assert from "node:assert/strict"; import { chromium } from "playwright-core"; const username = process.env.E2E_USERNAME ?? "regular-user"; const password = process.env.E2E_PASSWORD; assert.ok(password, "E2E_PASSWORD must be set"); 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(); let authorizationUrl; page.on("request", (request) => { if (request.url().includes("/protocol/openid-connect/auth")) { authorizationUrl = new URL(request.url()); } }); await page.goto("http://localhost:8088"); await page.locator("#login").click(); await page.waitForURL(/localhost:8080/u); await page.locator("#username").fill(username); await page.locator("#password").fill(password); await page.locator("#kc-login").click(); await page.waitForURL("http://localhost:8088/"); await page.locator('[data-authenticated="true"]').waitFor(); assert.equal(authorizationUrl?.searchParams.get("response_type"), "code"); assert.equal(authorizationUrl?.searchParams.get("code_challenge_method"), "S256"); assert.ok(authorizationUrl?.searchParams.get("code_challenge")); await page.evaluate(() => { const originalFetch = window.fetch.bind(window); window.__xssProbe = { authorization: null }; window.fetch = (input, init = {}) => { const headers = new Headers( init.headers ?? (input instanceof Request ? input.headers : undefined), ); const authorization = headers.get("Authorization"); if (authorization) { window.__xssProbe.authorization = authorization; } return originalFetch(input, init); }; }); await page.locator("#call-api").click(); await page.waitForFunction(() => { const text = document.querySelector("#result")?.textContent ?? ""; return text.includes('"httpStatus": 200'); }); const capturedAuthorization = await page.evaluate( () => window.__xssProbe.authorization, ); assert.match(capturedAuthorization, /^Bearer /u); const accessToken = capturedAuthorization.slice("Bearer ".length); const payload = JSON.parse( Buffer.from(accessToken.split(".")[1], "base64url").toString("utf8"), ); const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud]; assert.ok( audiences.includes("keycloak-pattern-api"), "access token must target keycloak-pattern-api", ); const storageSnapshot = await page.evaluate(() => ({ localStorage: Object.values(localStorage), sessionStorage: Object.values(sessionStorage), })); assert.equal( JSON.stringify(storageSnapshot).includes(accessToken), false, "access token must not be persisted in Web Storage", ); assert.ok( capturedAuthorization, "runtime XSS-style fetch hooking can still observe a memory-only bearer token", ); if (process.env.WRONG_AUDIENCE_URL) { const response = await fetch(process.env.WRONG_AUDIENCE_URL, { headers: { Authorization: `Bearer ${accessToken}` }, }); assert.equal( response.status, 401, "the same signed token must fail when the Resource Server expects another audience", ); } if (process.env.WRONG_ISSUER_URL) { const response = await fetch(process.env.WRONG_ISSUER_URL, { headers: { Authorization: `Bearer ${accessToken}` }, }); assert.equal( response.status, 401, "the same signed token must fail when the Resource Server expects another issuer", ); } await page.reload(); await page.locator('[data-authenticated="false"]').waitFor(); assert.equal( await page.evaluate( (token) => JSON.stringify({ localStorage: Object.values(localStorage), sessionStorage: Object.values(sessionStorage), }).includes(token), accessToken, ), false, "reload must clear the memory-only token without persisting it", ); console.log( "pattern1 browser verified: PKCE, audience/issuer negatives, persistent token 0, runtime fetch hook observes bearer, reload clears token", ); } finally { await browser.close(); }