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; const frontendUrl = "http://localhost:8088/"; const tokenEndpoint = "http://localhost:8080/realms/keycloak-patterns/protocol/openid-connect/token"; const revokeEndpoint = "http://localhost:8080/realms/keycloak-patterns/protocol/openid-connect/revoke"; assert.ok(password, "E2E_PASSWORD must be set"); async function login(page) { await page.locator("#login").click(); await page.waitForURL(/localhost:8080/u); await page.locator("#username").waitFor(); const tokenResponsePromise = page.waitForResponse((response) => response.url() === tokenEndpoint && response.request().postData()?.includes("grant_type=authorization_code"), ); await page.locator("#username").fill(username); await page.locator("#password").fill(password); await page.locator("#kc-login").click(); const tokenResponse = await tokenResponsePromise; assert.equal(tokenResponse.status(), 200); const tokenSet = await tokenResponse.json(); assert.ok(tokenSet.access_token); assert.ok(tokenSet.refresh_token); assert.ok(tokenSet.id_token); await page.waitForURL(frontendUrl); await page.locator('[data-authenticated="true"]').waitFor(); return tokenSet; } async function postForm(url, values) { return fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(values), }); } async function refresh(refreshToken) { return postForm(tokenEndpoint, { grant_type: "refresh_token", client_id: "spa-public", refresh_token: refreshToken, }); } async function callResource(accessToken, url = "http://localhost:8081/api/me") { return fetch(url, { headers: { Authorization: `Bearer ${accessToken}` }, }); } 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(frontendUrl); const firstTokenSet = await login(page); 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); assert.equal(accessToken, firstTokenSet.access_token); 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")); const storageSnapshot = await page.evaluate(() => ({ localStorage: Object.values(localStorage), sessionStorage: Object.values(sessionStorage), })); assert.equal(JSON.stringify(storageSnapshot).includes(accessToken), false); assert.ok(capturedAuthorization); if (process.env.WRONG_AUDIENCE_URL) { assert.equal( (await callResource(accessToken, process.env.WRONG_AUDIENCE_URL)).status, 401, ); } if (process.env.WRONG_ISSUER_URL) { assert.equal( (await callResource(accessToken, process.env.WRONG_ISSUER_URL)).status, 401, ); } const logoutRequestPromise = page.waitForRequest((request) => request.url().includes("/protocol/openid-connect/logout"), ); await page.locator("#logout").click(); const logoutRequest = await logoutRequestPromise; assert.ok(new URL(logoutRequest.url()).searchParams.get("id_token_hint")); await page.waitForURL(frontendUrl); await page.locator('[data-authenticated="false"]').waitFor(); const secondTokenSet = await login(page); const rotatedResponse = await refresh(secondTokenSet.refresh_token); assert.equal(rotatedResponse.status, 200); const rotated = await rotatedResponse.json(); assert.ok(rotated.refresh_token); assert.notEqual(rotated.refresh_token, secondTokenSet.refresh_token); const reusedOldResponse = await refresh(secondTokenSet.refresh_token); assert.notEqual( reusedOldResponse.status, 200, "a consumed refresh token must not be accepted again", ); const rotatedAfterReuseResponse = await refresh(rotated.refresh_token); const rotatedAfterReuseStatus = rotatedAfterReuseResponse.status; assert.ok([200, 400, 401].includes(rotatedAfterReuseStatus)); assert.equal( (await callResource(rotated.access_token)).status, 200, "a locally validated access JWT remains usable until exp", ); await context.clearCookies(); await page.reload(); await page.locator('[data-authenticated="false"]').waitFor(); const thirdTokenSet = await login(page); const revokeResponse = await postForm(revokeEndpoint, { token: thirdTokenSet.refresh_token, token_type_hint: "refresh_token", client_id: "spa-public", }); assert.equal(revokeResponse.status, 200); assert.notEqual((await refresh(thirdTokenSet.refresh_token)).status, 200); assert.equal( (await callResource(thirdTokenSet.access_token)).status, 200, "refresh revoke is not an immediate deny-list for a stateless access JWT", ); 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), thirdTokenSet.access_token, ), false, ); console.log( "pattern1 verified: PKCE, aud/iss negatives, memory/XSS boundary, logout, RT rotation/reuse, revoke-vs-stateless JWT" + ` (RT2 after RT1 reuse: ${rotatedAfterReuseStatus})`, ); } finally { await browser.close(); }