125 lines
4.2 KiB
JavaScript
125 lines
4.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { chromium } from "playwright-core";
|
|
|
|
const expectation = process.env.FIRST_BROKER_EXPECTATION;
|
|
assert.ok(
|
|
expectation === "vulnerable" || expectation === "secure",
|
|
"FIRST_BROKER_EXPECTATION must be vulnerable or secure",
|
|
);
|
|
|
|
const keycloakUrl = process.env.KEYCLOAK_URL ?? "http://localhost:8080";
|
|
const adminUsername = process.env.KC_BOOTSTRAP_ADMIN_USERNAME;
|
|
const adminPassword = process.env.KC_BOOTSTRAP_ADMIN_PASSWORD;
|
|
const mockPassword = process.env.MOCK_GOOGLE_USER_PASSWORD;
|
|
assert.ok(adminUsername && adminPassword && mockPassword);
|
|
|
|
async function adminToken() {
|
|
const body = new URLSearchParams({
|
|
client_id: "admin-cli",
|
|
grant_type: "password",
|
|
username: adminUsername,
|
|
password: adminPassword,
|
|
});
|
|
const response = await fetch(
|
|
`${keycloakUrl}/realms/master/protocol/openid-connect/token`,
|
|
{ method: "POST", body },
|
|
);
|
|
assert.equal(response.status, 200);
|
|
return (await response.json()).access_token;
|
|
}
|
|
|
|
async function regularUser(token) {
|
|
const response = await fetch(
|
|
`${keycloakUrl}/admin/realms/keycloak-patterns/users?username=regular-user&exact=true`,
|
|
{ headers: { Authorization: `Bearer ${token}` } },
|
|
);
|
|
assert.equal(response.status, 200);
|
|
const users = await response.json();
|
|
assert.equal(users.length, 1);
|
|
return users[0];
|
|
}
|
|
|
|
async function federatedIdentities(token, userId) {
|
|
const response = await fetch(
|
|
`${keycloakUrl}/admin/realms/keycloak-patterns/users/${userId}/federated-identity`,
|
|
{ headers: { Authorization: `Bearer ${token}` } },
|
|
);
|
|
assert.equal(response.status, 200);
|
|
return response.json();
|
|
}
|
|
|
|
async function removeMockLink(token, userId) {
|
|
const identities = await federatedIdentities(token, userId);
|
|
if (identities.some(({ identityProvider }) => identityProvider === "mock-google")) {
|
|
const response = await fetch(
|
|
`${keycloakUrl}/admin/realms/keycloak-patterns/users/${userId}/federated-identity/mock-google`,
|
|
{
|
|
method: "DELETE",
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
},
|
|
);
|
|
assert.equal(response.status, 204);
|
|
}
|
|
}
|
|
|
|
const token = await adminToken();
|
|
const user = await regularUser(token);
|
|
await removeMockLink(token, user.id);
|
|
|
|
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 authorizationUrl = new URL(
|
|
`${keycloakUrl}/realms/keycloak-patterns/protocol/openid-connect/auth`,
|
|
);
|
|
authorizationUrl.search = new URLSearchParams({
|
|
client_id: "spa-public",
|
|
redirect_uri: "http://localhost:8088/",
|
|
response_type: "code",
|
|
scope: "openid profile email",
|
|
state: `first-broker-${expectation}`,
|
|
nonce: `nonce-${expectation}`,
|
|
code_challenge: "K2qUEfBl-nQvF2gB4dNxC2zYVwZc1CVnZb5CsX2L7fI",
|
|
code_challenge_method: "S256",
|
|
kc_idp_hint: "mock-google",
|
|
});
|
|
|
|
await page.goto(authorizationUrl.toString());
|
|
await page.waitForURL(/\/realms\/mock-google\//u);
|
|
await page.locator("#username").fill("mock-collision-user");
|
|
await page.locator("#password").fill(mockPassword);
|
|
await page.locator("#kc-login").click();
|
|
await page.waitForLoadState("domcontentloaded");
|
|
|
|
if (expectation === "vulnerable") {
|
|
await page.waitForURL(/localhost:8088\/\?.*code=/u);
|
|
const identities = await federatedIdentities(token, user.id);
|
|
assert.equal(
|
|
identities.some(({ identityProvider }) => identityProvider === "mock-google"),
|
|
true,
|
|
"unsafe AutoLink should attach the attacker-controlled identity",
|
|
);
|
|
await removeMockLink(token, user.id);
|
|
} else {
|
|
assert.match(page.url(), /\/realms\/keycloak-patterns\//u);
|
|
const body = (await page.locator("body").innerText()).toLowerCase();
|
|
assert.match(body, /account already exists|link existing account|existing account/u);
|
|
const identities = await federatedIdentities(token, user.id);
|
|
assert.equal(
|
|
identities.some(({ identityProvider }) => identityProvider === "mock-google"),
|
|
false,
|
|
"Confirm Link must not attach the identity without ownership proof",
|
|
);
|
|
}
|
|
|
|
console.log(`first broker login ${expectation} case verified`);
|
|
} finally {
|
|
await browser.close();
|
|
}
|