merge: broker claim-to-role mapping

This commit is contained in:
donghyeon-ka
2026-07-25 16:26:59 +09:00
6 changed files with 125 additions and 1 deletions
+18
View File
@@ -0,0 +1,18 @@
# Google claim-to-role mapping
`hd=example.test`인 upstream OIDC identity에는 Keycloak realm role
`employee-role`을 부여한다. 매핑 키는 email이 아니라 Google subject이며,
role 조건에 쓰는 `hd` claim은 mock provider와 실제 Google provider에서 같은
계약을 사용한다.
Realm import는 `oidc-role-idp-mapper`를 선언한다. 실제 Google 설정 스크립트도
같은 mapper를 upsert한다. 따라서 재실행해도 mapper가 중복되지 않는다.
검증:
```sh
./scripts/verify-google-claim-to-role.sh
```
검증기는 mock Google 로그인, Authorization Code + PKCE 교환, 최종 Keycloak
access token의 `realm_access.roles`를 차례로 확인한다.
+68
View File
@@ -0,0 +1,68 @@
import assert from "node:assert/strict";
import { createHash, randomBytes } from "node:crypto";
import { chromium } from "playwright-core";
const keycloakUrl = process.env.KEYCLOAK_URL ?? "http://localhost:8080";
const password = process.env.MOCK_GOOGLE_USER_PASSWORD;
assert.ok(password);
const verifier = randomBytes(48).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
const redirectUri = "http://localhost:8088/";
const browser = await chromium.launch({
executablePath: process.env.CHROME_BIN ?? "/usr/bin/google-chrome",
headless: true,
args: ["--no-sandbox"],
});
try {
const page = await browser.newPage();
const authorizationUrl = new URL(
`${keycloakUrl}/realms/keycloak-patterns/protocol/openid-connect/auth`,
);
authorizationUrl.search = new URLSearchParams({
client_id: "spa-public",
redirect_uri: redirectUri,
response_type: "code",
scope: "openid profile email",
state: crypto.randomUUID(),
nonce: crypto.randomUUID(),
code_challenge: challenge,
code_challenge_method: "S256",
kc_idp_hint: "mock-google",
prompt: "login",
});
await page.goto(authorizationUrl.toString());
await page.waitForURL(/\/realms\/mock-google\//u);
await page.locator("#username").fill("mock-new-user");
await page.locator("#password").fill(password);
await page.locator("#kc-login").click();
await page.waitForURL(/localhost:8088\/\?.*code=/u);
const code = new URL(page.url()).searchParams.get("code");
assert.ok(code);
const response = await fetch(
`${keycloakUrl}/realms/keycloak-patterns/protocol/openid-connect/token`,
{
method: "POST",
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: "spa-public",
redirect_uri: redirectUri,
code,
code_verifier: verifier,
}),
},
);
assert.equal(response.status, 200);
const tokens = await response.json();
const payload = JSON.parse(
Buffer.from(tokens.access_token.split(".")[1], "base64url").toString(),
);
assert.ok(payload.realm_access.roles.includes("employee-role"));
console.log("Claim-to-role verified: hd=example.test -> employee-role");
} finally {
await browser.close();
}
+2 -1
View File
@@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"test:first-broker": "node first-broker-login.mjs",
"test:claim-mapping": "node claim-mapping.mjs"
"test:claim-mapping": "node claim-mapping.mjs",
"test:claim-to-role": "node claim-to-role.mjs"
},
"dependencies": {
"playwright-core": "1.55.1"
@@ -24,6 +24,10 @@
{
"name": "user-role",
"description": "Regular authenticated user role"
},
{
"name": "employee-role",
"description": "Assigned to brokered identities whose hosted-domain claim is example.test"
}
]
},
@@ -213,6 +217,17 @@
"claim": "hd",
"user.attribute": "hd"
}
},
{
"name": "mock-google-example-domain-role",
"identityProviderAlias": "mock-google",
"identityProviderMapper": "oidc-role-idp-mapper",
"config": {
"syncMode": "INHERIT",
"claim": "hd",
"claim.value": "example.test",
"role": "employee-role"
}
}
],
"users": [
+4
View File
@@ -144,6 +144,10 @@ upsert_mapper \
"google-hosted-domain" \
"oidc-user-attribute-idp-mapper" \
'{"syncMode":"INHERIT","claim":"hd","user.attribute":"hd"}'
upsert_mapper \
"google-example-domain-role" \
"oidc-role-idp-mapper" \
'{"syncMode":"INHERIT","claim":"hd","claim.value":"example.test","role":"employee-role"}'
echo "Google Identity Provider $action for realm '$realm'"
echo "Register this exact Google redirect URI:"
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env sh
set -eu
set -a
. ./.env
set +a
./scripts/configure-broker-user-profile.sh >/dev/null
./scripts/set-first-broker-login-mode.sh secure
docker compose exec -T keycloak \
/opt/keycloak/bin/kcadm.sh create roles -r keycloak-patterns \
-s name=employee-role \
-s 'description=Assigned from the upstream hd claim' >/dev/null 2>&1 || true
cd google-e2e
npm install --ignore-scripts
npm run test:claim-to-role