Compare commits

..
12 changed files with 430 additions and 0 deletions
+7
View File
@@ -10,8 +10,15 @@ POSTGRES_PASSWORD=change-me-postgres-password
TOKEN_MEDIATING_CLIENT_SECRET=change-me-token-mediating-client-secret TOKEN_MEDIATING_CLIENT_SECRET=change-me-token-mediating-client-secret
BFF_CLIENT_SECRET=change-me-bff-client-secret BFF_CLIENT_SECRET=change-me-bff-client-secret
EDGE_PROXY_CLIENT_SECRET=change-me-edge-proxy-client-secret EDGE_PROXY_CLIENT_SECRET=change-me-edge-proxy-client-secret
MOCK_GOOGLE_BROKER_CLIENT_SECRET=change-me-mock-google-broker-client-secret
ADMIN_USER_PASSWORD=change-me-admin-user-password ADMIN_USER_PASSWORD=change-me-admin-user-password
REGULAR_USER_PASSWORD=change-me-regular-user-password REGULAR_USER_PASSWORD=change-me-regular-user-password
MOCK_GOOGLE_USER_PASSWORD=change-me-mock-google-user-password
# Optional real-Google profile. These are consumed only by
# scripts/configure-google-idp.sh and must never be committed with real values.
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Port 80 is the single-EC2 target. 8088 avoids common local port conflicts. # Port 80 is the single-EC2 target. 8088 avoids common local port conflicts.
NGINX_PORT=8088 NGINX_PORT=8088
+3
View File
@@ -5,3 +5,6 @@
backend/target/ backend/target/
build/ build/
e2e/node_modules/
frontend/node_modules/
frontend/dist/
+7
View File
@@ -1,5 +1,12 @@
# Keycloak Authentication Patterns # Keycloak Authentication Patterns
The 39-branch implementation registry is documented in
[`docs/keycloak-branch-index.md`](docs/keycloak-branch-index.md).
Google brokering has a credential-free local OIDC harness and an opt-in
real-Google profile described in
[`docs/google-idp-brokering.md`](docs/google-idp-brokering.md).
Keycloak을 중심으로 네 가지 브라우저 인증 통합 패턴을 같은 로컬 Keycloak을 중심으로 네 가지 브라우저 인증 통합 패턴을 같은 로컬
인프라에서 비교하는 학습 프로젝트입니다. 인프라에서 비교하는 학습 프로젝트입니다.
+2
View File
@@ -38,8 +38,10 @@ services:
TOKEN_MEDIATING_CLIENT_SECRET: ${TOKEN_MEDIATING_CLIENT_SECRET:?set TOKEN_MEDIATING_CLIENT_SECRET in .env} TOKEN_MEDIATING_CLIENT_SECRET: ${TOKEN_MEDIATING_CLIENT_SECRET:?set TOKEN_MEDIATING_CLIENT_SECRET in .env}
BFF_CLIENT_SECRET: ${BFF_CLIENT_SECRET:?set BFF_CLIENT_SECRET in .env} BFF_CLIENT_SECRET: ${BFF_CLIENT_SECRET:?set BFF_CLIENT_SECRET in .env}
EDGE_PROXY_CLIENT_SECRET: ${EDGE_PROXY_CLIENT_SECRET:?set EDGE_PROXY_CLIENT_SECRET in .env} EDGE_PROXY_CLIENT_SECRET: ${EDGE_PROXY_CLIENT_SECRET:?set EDGE_PROXY_CLIENT_SECRET in .env}
MOCK_GOOGLE_BROKER_CLIENT_SECRET: ${MOCK_GOOGLE_BROKER_CLIENT_SECRET:?set MOCK_GOOGLE_BROKER_CLIENT_SECRET in .env}
ADMIN_USER_PASSWORD: ${ADMIN_USER_PASSWORD:?set ADMIN_USER_PASSWORD in .env} ADMIN_USER_PASSWORD: ${ADMIN_USER_PASSWORD:?set ADMIN_USER_PASSWORD in .env}
REGULAR_USER_PASSWORD: ${REGULAR_USER_PASSWORD:?set REGULAR_USER_PASSWORD in .env} REGULAR_USER_PASSWORD: ${REGULAR_USER_PASSWORD:?set REGULAR_USER_PASSWORD in .env}
MOCK_GOOGLE_USER_PASSWORD: ${MOCK_GOOGLE_USER_PASSWORD:?set MOCK_GOOGLE_USER_PASSWORD in .env}
ports: ports:
- "127.0.0.1:8080:8080" - "127.0.0.1:8080:8080"
volumes: volumes:
+28
View File
@@ -0,0 +1,28 @@
# Google IdP brokering
Keycloak is the only issuer trusted by AP1AP4. Google is an upstream Identity
Provider; applications do not receive or validate a Google token.
## Two verification profiles
The default local profile imports a second Keycloak realm named `mock-google`.
It acts as a controllable OIDC provider and allows tests to choose claims such
as a duplicate email, `email_verified=false`, `hd`, and `picture`. This is the
safe way to reproduce an unsafe email auto-link without impersonating a real
Google account.
The real-Google profile is configured explicitly:
1. Create a Google OAuth **Web application**.
2. Register the exact redirect URI printed by
`./scripts/configure-google-idp.sh`.
3. Put `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in ignored `.env`.
4. Start the stack and run the configuration script.
The script writes `providerId=google`, `trustEmail=false`, minimal
`openid profile email` scopes, and `syncMode=IMPORT` through the Keycloak Admin
API. Credentials are never written to the realm export or repository.
Google requires a public HTTPS redirect for non-local deployments. Local mock
verification proves the Keycloak brokering boundary; a real Google login is a
separate credentialed acceptance profile.
+29
View File
@@ -0,0 +1,29 @@
# Keycloak branch implementation index
The source inventory contains 39 `feature-keycloak-*.md` branch notes. This
repository preserves one local Git feature branch for every note and merges it
with `--no-ff` into either the common `develop` baseline or one of the four
authentication-pattern branches.
| Target | Meaning |
|---|---|
| `common` | Shared realm, federation, deployment, or governance contract. Merge into `develop`, then propagate to AP1AP4. |
| `ap1` | Browser-based OAuth client: vanilla SPA, Authorization Code + PKCE, Resource Server. |
| `ap2` | Token-mediating confidential backend: browser receives access token only. |
| `ap3` | BFF: backend owns every OAuth token and browser owns only a session cookie. |
| `ap4` | Edge forward-auth: oauth2-proxy/Nginx owns login and backend trusts an isolated identity header. |
The machine-readable registry is
[`keycloak-branch-manifest.tsv`](keycloak-branch-manifest.tsv). Run:
```bash
./scripts/audit-keycloak-branches.sh
```
The audit succeeds only when all 39 note names have matching local feature
branches and each feature tip is reachable from its declared target branch.
Google credentials are never committed. The default local acceptance harness
uses a second Keycloak realm as a controllable OIDC provider so claim mapping
and unsafe-linking failure paths can be reproduced. A real Google login remains
an explicit credentialed/public-HTTPS verification profile.
+40
View File
@@ -0,0 +1,40 @@
branch target delivery
feature/keycloak-account-linking-spa-ux ap1 documented-and-contract-tested
feature/keycloak-account-linking-sub-vs-email common documented-and-contract-tested
feature/keycloak-bff-csrf-samesite-defense ap3 locally-verified
feature/keycloak-bff-oauth2login-session ap3 locally-verified
feature/keycloak-bff-vs-spa-direct ap3 documented
feature/keycloak-docker-compose-stack common locally-verified
feature/keycloak-edge-forwardauth-google-federation ap4 documented-and-config-tested
feature/keycloak-edge-forwardauth-no-google ap4 documented-and-config-tested
feature/keycloak-federation-spa-zero-change ap1 contract-tested
feature/keycloak-first-broker-login-flow common locally-verified-with-mock-idp
feature/keycloak-four-pattern-tradeoff-matrix common documented-and-evidence-linked
feature/keycloak-google-claim-attribute-mapping common locally-verified-with-mock-idp
feature/keycloak-google-redirect-uri-policy common config-tested
feature/keycloak-header-spoofing-defense ap4 locally-verified
feature/keycloak-https-termination-caddy-nginx common config-tested
feature/keycloak-idp-brokering-google-client common locally-verified-with-mock-idp
feature/keycloak-idp-mappers-claim-to-role common locally-verified-with-mock-idp
feature/keycloak-internal-spa-direct-google-federation ap1 documented-and-contract-tested
feature/keycloak-internal-spa-direct-no-google ap1 documented-and-contract-tested
feature/keycloak-iss-claim-hostname-mismatch ap1 locally-verified
feature/keycloak-nginx-auth-request-integration ap4 locally-verified
feature/keycloak-oauth2-proxy-oidc-flow ap4 locally-verified
feature/keycloak-patterns common governance
feature/keycloak-pkce-flow-stages ap1 contract-tested
feature/keycloak-public-domain-tunneling common config-tested
feature/keycloak-realm-client-export common locally-verified
feature/keycloak-refresh-rotation-and-logout ap1 locally-verified
feature/keycloak-refresh-token-rotation ap1 contract-tested
feature/keycloak-reverse-proxy-headers common config-tested
feature/keycloak-single-ec2-google-federation ap1 documented-and-config-tested
feature/keycloak-single-ec2-no-google ap1 documented-and-contract-tested
feature/keycloak-spa-token-storage-tradeoff ap1 locally-verified
feature/keycloak-spring-rs-audience-validator ap1 locally-verified
feature/keycloak-spring-rs-role-mapping ap1 locally-verified
feature/keycloak-three-leg-trust-chain ap1 documented-and-contract-tested
feature/keycloak-token-mediating-access-handoff ap2 locally-verified
feature/keycloak-token-mediating-confidential-client ap2 locally-verified
feature/keycloak-traefik-forwardauth-alternative ap4 config-tested
feature/keycloak-vanilla-js-spa-pkce ap1 locally-verified
1 branch target delivery
2 feature/keycloak-account-linking-spa-ux ap1 documented-and-contract-tested
3 feature/keycloak-account-linking-sub-vs-email common documented-and-contract-tested
4 feature/keycloak-bff-csrf-samesite-defense ap3 locally-verified
5 feature/keycloak-bff-oauth2login-session ap3 locally-verified
6 feature/keycloak-bff-vs-spa-direct ap3 documented
7 feature/keycloak-docker-compose-stack common locally-verified
8 feature/keycloak-edge-forwardauth-google-federation ap4 documented-and-config-tested
9 feature/keycloak-edge-forwardauth-no-google ap4 documented-and-config-tested
10 feature/keycloak-federation-spa-zero-change ap1 contract-tested
11 feature/keycloak-first-broker-login-flow common locally-verified-with-mock-idp
12 feature/keycloak-four-pattern-tradeoff-matrix common documented-and-evidence-linked
13 feature/keycloak-google-claim-attribute-mapping common locally-verified-with-mock-idp
14 feature/keycloak-google-redirect-uri-policy common config-tested
15 feature/keycloak-header-spoofing-defense ap4 locally-verified
16 feature/keycloak-https-termination-caddy-nginx common config-tested
17 feature/keycloak-idp-brokering-google-client common locally-verified-with-mock-idp
18 feature/keycloak-idp-mappers-claim-to-role common locally-verified-with-mock-idp
19 feature/keycloak-internal-spa-direct-google-federation ap1 documented-and-contract-tested
20 feature/keycloak-internal-spa-direct-no-google ap1 documented-and-contract-tested
21 feature/keycloak-iss-claim-hostname-mismatch ap1 locally-verified
22 feature/keycloak-nginx-auth-request-integration ap4 locally-verified
23 feature/keycloak-oauth2-proxy-oidc-flow ap4 locally-verified
24 feature/keycloak-patterns common governance
25 feature/keycloak-pkce-flow-stages ap1 contract-tested
26 feature/keycloak-public-domain-tunneling common config-tested
27 feature/keycloak-realm-client-export common locally-verified
28 feature/keycloak-refresh-rotation-and-logout ap1 locally-verified
29 feature/keycloak-refresh-token-rotation ap1 contract-tested
30 feature/keycloak-reverse-proxy-headers common config-tested
31 feature/keycloak-single-ec2-google-federation ap1 documented-and-config-tested
32 feature/keycloak-single-ec2-no-google ap1 documented-and-contract-tested
33 feature/keycloak-spa-token-storage-tradeoff ap1 locally-verified
34 feature/keycloak-spring-rs-audience-validator ap1 locally-verified
35 feature/keycloak-spring-rs-role-mapping ap1 locally-verified
36 feature/keycloak-three-leg-trust-chain ap1 documented-and-contract-tested
37 feature/keycloak-token-mediating-access-handoff ap2 locally-verified
38 feature/keycloak-token-mediating-confidential-client ap2 locally-verified
39 feature/keycloak-traefik-forwardauth-alternative ap4 config-tested
40 feature/keycloak-vanilla-js-spa-pkce ap1 locally-verified
@@ -124,6 +124,36 @@
} }
} }
], ],
"identityProviders": [
{
"alias": "mock-google",
"displayName": "Mock Google (local verification)",
"providerId": "oidc",
"enabled": true,
"updateProfileFirstLoginMode": "off",
"trustEmail": false,
"storeToken": false,
"addReadTokenRoleOnCreate": false,
"authenticateByDefault": false,
"linkOnly": false,
"firstBrokerLoginFlowAlias": "first broker login",
"config": {
"clientId": "mock-google-broker",
"clientSecret": "${MOCK_GOOGLE_BROKER_CLIENT_SECRET}",
"authorizationUrl": "http://localhost:8080/realms/mock-google/protocol/openid-connect/auth",
"tokenUrl": "http://keycloak:8080/realms/mock-google/protocol/openid-connect/token",
"userInfoUrl": "http://keycloak:8080/realms/mock-google/protocol/openid-connect/userinfo",
"issuer": "http://localhost:8080/realms/mock-google",
"jwksUrl": "http://keycloak:8080/realms/mock-google/protocol/openid-connect/certs",
"useJwksUrl": "true",
"validateSignature": "true",
"defaultScope": "openid profile email",
"syncMode": "IMPORT",
"pkceEnabled": "true",
"pkceMethod": "S256"
}
}
],
"users": [ "users": [
{ {
"username": "admin-user", "username": "admin-user",
+71
View File
@@ -0,0 +1,71 @@
{
"realm": "mock-google",
"displayName": "Controllable Google OIDC Test Provider",
"enabled": true,
"sslRequired": "external",
"registrationAllowed": false,
"resetPasswordAllowed": false,
"editUsernameAllowed": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"bruteForceProtected": true,
"clients": [
{
"clientId": "mock-google-broker",
"name": "Main Realm Identity Broker",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"clientAuthenticatorType": "client-secret",
"secret": "${MOCK_GOOGLE_BROKER_CLIENT_SECRET}",
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"redirectUris": [
"http://localhost:8080/realms/keycloak-patterns/broker/mock-google/endpoint"
],
"webOrigins": []
}
],
"users": [
{
"username": "mock-new-user",
"enabled": true,
"email": "broker-new-user@example.test",
"emailVerified": true,
"firstName": "Broker",
"lastName": "New",
"credentials": [
{
"type": "password",
"value": "${MOCK_GOOGLE_USER_PASSWORD}",
"temporary": false
}
]
},
{
"username": "mock-collision-user",
"enabled": true,
"email": "regular-user@example.test",
"emailVerified": false,
"firstName": "Broker",
"lastName": "Collision",
"attributes": {
"hd": [
"example.test"
],
"picture": [
"https://images.example.test/mock-collision-user.png"
]
},
"credentials": [
{
"type": "password",
"value": "${MOCK_GOOGLE_USER_PASSWORD}",
"temporary": false
}
]
}
]
}
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env sh
set -eu
manifest="${1:-docs/keycloak-branch-manifest.tsv}"
notes_dir="${KEYCLOAK_BRANCH_NOTES_DIR:-/home/donghyeon/workspace/ai-tools/llm-wiki/raw/branch-notes}"
expected_count="$(awk 'NR > 1 { count += 1 } END { print count + 0 }' "$manifest")"
if [ "$expected_count" -ne 39 ]; then
echo "manifest must contain exactly 39 Keycloak branches; found $expected_count" >&2
exit 1
fi
note_count="$(find "$notes_dir" -maxdepth 1 -type f -name 'feature-keycloak-*.md' | wc -l)"
if [ "$note_count" -ne 39 ]; then
echo "branch-note inventory must contain exactly 39 files; found $note_count" >&2
exit 1
fi
missing=0
unmerged=0
tab="$(printf '\t')"
while IFS="$tab" read -r branch target delivery; do
[ "$branch" = "branch" ] && continue
note_name="$(printf '%s\n' "$branch" |
sed 's#^feature/keycloak-#feature-keycloak-#').md"
if [ ! -f "$notes_dir/$note_name" ]; then
echo "missing branch note: $note_name" >&2
missing=$((missing + 1))
fi
if ! git show-ref --verify --quiet "refs/heads/$branch"; then
echo "missing local branch: $branch" >&2
missing=$((missing + 1))
continue
fi
case "$target" in
common) target_branch="develop" ;;
ap1) target_branch="develop-keycloak-pattern1" ;;
ap2) target_branch="develop-keycloak-pattern2" ;;
ap3) target_branch="develop-keycloak-pattern3" ;;
ap4) target_branch="develop-keycloak-pattern4" ;;
*)
echo "unknown target '$target' for $branch ($delivery)" >&2
exit 1
;;
esac
if ! git merge-base --is-ancestor "$branch" "$target_branch"; then
echo "feature tip is not merged: $branch -> $target_branch" >&2
unmerged=$((unmerged + 1))
fi
done < "$manifest"
if [ "$missing" -ne 0 ] || [ "$unmerged" -ne 0 ]; then
echo "Keycloak branch audit failed: missing=$missing unmerged=$unmerged" >&2
exit 1
fi
echo "Keycloak branch audit passed: 39/39 branches exist and are merged"
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env sh
set -eu
if [ ! -f .env ]; then
echo "missing .env" >&2
exit 1
fi
set -a
. ./.env
set +a
: "${KC_BOOTSTRAP_ADMIN_USERNAME:?set KC_BOOTSTRAP_ADMIN_USERNAME in .env}"
: "${KC_BOOTSTRAP_ADMIN_PASSWORD:?set KC_BOOTSTRAP_ADMIN_PASSWORD in .env}"
: "${GOOGLE_CLIENT_ID:?set GOOGLE_CLIENT_ID in .env}"
: "${GOOGLE_CLIENT_SECRET:?set GOOGLE_CLIENT_SECRET in .env}"
keycloak_url="${KEYCLOAK_URL:-http://localhost:8080}"
realm="${KEYCLOAK_REALM:-keycloak-patterns}"
admin_token="$(
curl -fsS \
-d client_id=admin-cli \
-d grant_type=password \
-d "username=$KC_BOOTSTRAP_ADMIN_USERNAME" \
-d "password=$KC_BOOTSTRAP_ADMIN_PASSWORD" \
"$keycloak_url/realms/master/protocol/openid-connect/token" |
jq -er .access_token
)"
payload="$(
jq -n \
--arg client_id "$GOOGLE_CLIENT_ID" \
--arg client_secret "$GOOGLE_CLIENT_SECRET" \
'{
alias: "google",
displayName: "Sign in with Google",
providerId: "google",
enabled: true,
updateProfileFirstLoginMode: "off",
trustEmail: false,
storeToken: false,
addReadTokenRoleOnCreate: false,
authenticateByDefault: false,
linkOnly: false,
firstBrokerLoginFlowAlias: "first broker login",
config: {
clientId: $client_id,
clientSecret: $client_secret,
defaultScope: "openid profile email",
syncMode: "IMPORT"
}
}'
)"
endpoint="$keycloak_url/admin/realms/$realm/identity-provider/instances"
status="$(
curl -sS -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer $admin_token" \
"$endpoint/google"
)"
if [ "$status" = "200" ]; then
curl -fsS -X PUT \
-H "Authorization: Bearer $admin_token" \
-H "Content-Type: application/json" \
--data "$payload" \
"$endpoint/google"
action="updated"
else
curl -fsS -X POST \
-H "Authorization: Bearer $admin_token" \
-H "Content-Type: application/json" \
--data "$payload" \
"$endpoint"
action="created"
fi
echo "Google Identity Provider $action for realm '$realm'"
echo "Register this exact Google redirect URI:"
echo "$keycloak_url/realms/$realm/broker/google/endpoint"
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env sh
set -eu
if [ ! -f .env ]; then
echo "missing .env" >&2
exit 1
fi
set -a
. ./.env
set +a
keycloak_url="${KEYCLOAK_URL:-http://localhost:8080}"
admin_token="$(
curl -fsS \
-d client_id=admin-cli \
-d grant_type=password \
-d "username=$KC_BOOTSTRAP_ADMIN_USERNAME" \
-d "password=$KC_BOOTSTRAP_ADMIN_PASSWORD" \
"$keycloak_url/realms/master/protocol/openid-connect/token" |
jq -er .access_token
)"
idp="$(
curl -fsS \
-H "Authorization: Bearer $admin_token" \
"$keycloak_url/admin/realms/keycloak-patterns/identity-provider/instances/mock-google"
)"
printf '%s' "$idp" | jq -e '
.providerId == "oidc" and
.enabled == true and
.trustEmail == false and
.config.clientId == "mock-google-broker" and
.config.defaultScope == "openid profile email" and
.config.syncMode == "IMPORT" and
.config.validateSignature == "true"
' >/dev/null
client="$(
curl -fsS \
-H "Authorization: Bearer $admin_token" \
"$keycloak_url/admin/realms/mock-google/clients?clientId=mock-google-broker"
)"
printf '%s' "$client" | jq -e '
length == 1 and
.[0].publicClient == false and
(.[0].redirectUris | index(
"http://localhost:8080/realms/keycloak-patterns/broker/mock-google/endpoint"
)) != null
' >/dev/null
location="$(
curl -sS -D - -o /dev/null \
"$keycloak_url/realms/keycloak-patterns/protocol/openid-connect/auth?client_id=spa-public&redirect_uri=http%3A%2F%2Flocalhost%3A8088%2F&response_type=code&scope=openid&code_challenge=K2qUEfBl-nQvF2gB4dNxC2zYVwZc1CVnZb5CsX2L7fI&code_challenge_method=S256&kc_idp_hint=mock-google" |
awk 'BEGIN { IGNORECASE=1 } /^Location:/ { print $2 }' |
tr -d '\r'
)"
case "$location" in
"$keycloak_url/realms/keycloak-patterns/broker/mock-google/login"*) ;;
*)
echo "broker did not redirect to the controllable OIDC provider: $location" >&2
exit 1
;;
esac
echo "Google broker contract verified with the local mock OIDC realm"