feat(ap2): hand off access token only
This commit is contained in:
+65
-7
@@ -4,6 +4,28 @@ 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,
|
||||
@@ -17,12 +39,7 @@ try {
|
||||
await page.goto("http://localhost:8082");
|
||||
await page.locator("#login").click();
|
||||
await page.waitForURL(/localhost:8080/u);
|
||||
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.waitForURL("http://localhost:8082/");
|
||||
await completeKeycloakLogin(page);
|
||||
|
||||
const boundaryResponsePromise = page.waitForResponse((response) =>
|
||||
response.url().endsWith("/token/boundary"),
|
||||
@@ -37,6 +54,43 @@ try {
|
||||
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);
|
||||
@@ -48,9 +102,13 @@ try {
|
||||
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 confidential client verified: server code exchange, server access/refresh custody, HttpOnly session",
|
||||
"pattern2 verified: server refresh custody, access-only handoff, direct browser resource call",
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
|
||||
Reference in New Issue
Block a user