feat(ap3): add oauth2Login session BFF
This commit is contained in:
Generated
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "keycloak-pattern-e2e",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "keycloak-pattern-e2e",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"playwright-core": "1.62.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.62.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz",
|
||||
"integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "keycloak-pattern-e2e",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test:pattern3": "node pattern3.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"playwright-core": "1.62.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
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:8083/") {
|
||||
return;
|
||||
}
|
||||
if (attempt === 1) {
|
||||
await page.goto(
|
||||
"http://localhost:8083/oauth2/authorization/keycloak",
|
||||
);
|
||||
await page.waitForURL(/localhost:8080/u);
|
||||
}
|
||||
}
|
||||
throw new Error(`Keycloak login did not return to AP3: ${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(request.url()));
|
||||
|
||||
await page.goto("http://localhost:8083");
|
||||
const authorizationRequestPromise = page.waitForRequest((request) =>
|
||||
request.url().includes(
|
||||
"/protocol/openid-connect/auth?response_type=code",
|
||||
),
|
||||
);
|
||||
await page.locator("#login").click();
|
||||
const authorizationRequest = await authorizationRequestPromise;
|
||||
const authorizationUrl = new URL(authorizationRequest.url());
|
||||
assert.equal(authorizationUrl.searchParams.get("client_id"), "bff-confidential");
|
||||
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 callbackRequest = browserRequests.find((url) =>
|
||||
url.startsWith("http://localhost:8083/login/oauth2/code/keycloak?"),
|
||||
);
|
||||
assert.ok(callbackRequest, "authorization response must use the BFF callback");
|
||||
|
||||
const boundaryResponsePromise = page.waitForResponse((response) =>
|
||||
response.url().endsWith("/bff/token-boundary"),
|
||||
);
|
||||
await page.locator("#inspect").click();
|
||||
const boundaryResponse = await boundaryResponsePromise;
|
||||
assert.equal(boundaryResponse.status(), 200);
|
||||
const boundary = await boundaryResponse.json();
|
||||
assert.equal(boundary.accessTokenStoredOnServer, true);
|
||||
assert.equal(boundary.refreshTokenStoredOnServer, true);
|
||||
assert.equal(boundary.browserTokenCount, 0);
|
||||
assert.equal(boundary.csrfProtectionEnabled, false);
|
||||
assert.equal(JSON.stringify(boundary).includes("access_token"), false);
|
||||
assert.equal(JSON.stringify(boundary).includes("refresh_token"), false);
|
||||
|
||||
const proxyResponsePromise = page.waitForResponse((response) =>
|
||||
response.url().endsWith("/bff/api/me"),
|
||||
);
|
||||
await page.locator("#call-bff").click();
|
||||
const proxyResponse = await proxyResponsePromise;
|
||||
assert.equal(proxyResponse.status(), 200);
|
||||
const resource = await proxyResponse.json();
|
||||
assert.equal(resource.username, "regular-user");
|
||||
assert.ok(resource.audience.includes("keycloak-pattern-api"));
|
||||
|
||||
assert.equal(
|
||||
browserRequests.some((url) => url.startsWith("http://localhost:8081/")),
|
||||
false,
|
||||
"the browser must not bypass the BFF",
|
||||
);
|
||||
assert.equal(
|
||||
browserRequests.some((url) =>
|
||||
url.includes("/protocol/openid-connect/token"),
|
||||
),
|
||||
false,
|
||||
"the token exchange must be server-to-server",
|
||||
);
|
||||
|
||||
const cookies = await context.cookies("http://localhost:8083/");
|
||||
const sessionCookie = cookies.find((cookie) => cookie.name === "AP3_SESSION");
|
||||
assert.ok(sessionCookie);
|
||||
assert.equal(sessionCookie.httpOnly, true);
|
||||
|
||||
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("AP3_SESSION"), false);
|
||||
|
||||
await page.goto("http://localhost:8088");
|
||||
const forgedResponsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url() === "http://localhost:8083/bff/api/preferences" &&
|
||||
response.request().method() === "POST",
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = "http://localhost:8083/bff/api/preferences";
|
||||
const input = document.createElement("input");
|
||||
input.name = "theme";
|
||||
input.value = "attacker";
|
||||
form.append(input);
|
||||
document.body.append(form);
|
||||
form.submit();
|
||||
});
|
||||
const forgedResponse = await forgedResponsePromise;
|
||||
assert.equal(forgedResponse.status(), 200);
|
||||
const forgedResult = await forgedResponse.json();
|
||||
assert.equal(forgedResult.updated, true);
|
||||
assert.equal(forgedResult.theme, "attacker");
|
||||
|
||||
console.log(
|
||||
"pattern3 BFF verified: browser token 0, session-only proxy 200, pre-defense CSRF reproduced",
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
Reference in New Issue
Block a user