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"); 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() === "http://localhost:8082/") { return; } if (attempt === 1) { await page.goto( "http://localhost:8082/oauth2/authorization/keycloak", ); await page.waitForURL(/localhost:8080/u); } } throw new Error(`Keycloak login did not return to AP2: ${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(); await page.goto("http://localhost:8082"); await page.locator("#login").click(); await page.waitForURL(/localhost:8080/u); await completeKeycloakLogin(page); const boundaryResponsePromise = page.waitForResponse((response) => response.url().endsWith("/token/boundary"), ); await page.locator("#inspect").click(); const boundaryResponse = await boundaryResponsePromise; assert.equal(boundaryResponse.status(), 200); const boundary = await boundaryResponse.json(); assert.equal(boundary.accessTokenStored, true); assert.equal(boundary.refreshTokenStored, true); assert.equal(boundary.browserReceivesRefreshToken, false); assert.equal(JSON.stringify(boundary).includes("refresh_token"), false); const accessResponsePromise = page.waitForResponse((response) => response.url().endsWith("/token/access"), ); const resourceResponsePromise = page.waitForResponse( (response) => response.url() === "http://localhost:8081/api/me" && response.request().method() === "GET", ); await page.locator("#call-api").click(); const accessResponse = await accessResponsePromise; assert.equal(accessResponse.status(), 200); assert.match(accessResponse.headers()["cache-control"], /no-store/u); const accessHandoff = await accessResponse.json(); assert.deepEqual( Object.keys(accessHandoff).sort(), ["access_token", "expires_at", "token_type"], ); assert.equal(accessHandoff.token_type, "Bearer"); assert.equal(typeof accessHandoff.access_token, "string"); assert.ok(accessHandoff.access_token.length > 100); assert.equal(JSON.stringify(accessHandoff).includes("refresh_token"), false); const [, payload] = accessHandoff.access_token.split("."); const claims = JSON.parse( Buffer.from(payload, "base64url").toString("utf8"), ); const audience = Array.isArray(claims.aud) ? claims.aud : [claims.aud]; assert.ok(audience.includes("keycloak-pattern-api")); const resourceResponse = await resourceResponsePromise; assert.equal(resourceResponse.status(), 200); const resource = await resourceResponse.json(); assert.equal(resource.username, "regular-user"); assert.ok(resource.audience.includes("keycloak-pattern-api")); await page.locator("#result").getByText('"resourceApiStatus": 200').waitFor(); const cookies = await context.cookies("http://localhost:8082/"); const sessionCookie = cookies.find((cookie) => cookie.name === "AP2_SESSION"); assert.ok(sessionCookie); assert.equal(sessionCookie.httpOnly, true); assert.equal(sessionCookie.sameSite, "Lax"); const storage = await page.evaluate(() => ({ localStorage: Object.values(localStorage), sessionStorage: Object.values(sessionStorage), })); assert.equal(JSON.stringify(storage).includes("refresh_token"), false); assert.equal( JSON.stringify(storage).includes(accessHandoff.access_token), false, ); console.log( "pattern2 verified: server refresh custody, access-only handoff, direct browser resource call", ); } finally { await browser.close(); }