From 2ee4b2af1c6747697ff95a5eb42f67d5b07f6703 Mon Sep 17 00:00:00 2001 From: donghyeon-ka Date: Sat, 25 Jul 2026 15:30:47 +0900 Subject: [PATCH] feat: add Google broker configuration profiles --- .env.example | 7 ++ README.md | 4 + docker-compose.yml | 2 + docs/google-idp-brokering.md | 28 +++++++ keycloak/import/keycloak-patterns-realm.json | 30 ++++++++ keycloak/import/mock-google-realm.json | 71 +++++++++++++++++ scripts/configure-google-idp.sh | 81 ++++++++++++++++++++ scripts/verify-google-broker-config.sh | 70 +++++++++++++++++ 8 files changed, 293 insertions(+) create mode 100644 docs/google-idp-brokering.md create mode 100644 keycloak/import/mock-google-realm.json create mode 100755 scripts/configure-google-idp.sh create mode 100755 scripts/verify-google-broker-config.sh diff --git a/.env.example b/.env.example index 1bc37f1..37c5628 100644 --- a/.env.example +++ b/.env.example @@ -10,8 +10,15 @@ POSTGRES_PASSWORD=change-me-postgres-password TOKEN_MEDIATING_CLIENT_SECRET=change-me-token-mediating-client-secret BFF_CLIENT_SECRET=change-me-bff-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 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. NGINX_PORT=8088 diff --git a/README.md b/README.md index b502271..29e6033 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ 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을 중심으로 네 가지 브라우저 인증 통합 패턴을 같은 로컬 인프라에서 비교하는 학습 프로젝트입니다. diff --git a/docker-compose.yml b/docker-compose.yml index 585bebf..73f52fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,8 +38,10 @@ services: 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} 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} 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: - "127.0.0.1:8080:8080" volumes: diff --git a/docs/google-idp-brokering.md b/docs/google-idp-brokering.md new file mode 100644 index 0000000..089c7f1 --- /dev/null +++ b/docs/google-idp-brokering.md @@ -0,0 +1,28 @@ +# Google IdP brokering + +Keycloak is the only issuer trusted by AP1–AP4. 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. diff --git a/keycloak/import/keycloak-patterns-realm.json b/keycloak/import/keycloak-patterns-realm.json index 9a4fae9..5ec8bd8 100644 --- a/keycloak/import/keycloak-patterns-realm.json +++ b/keycloak/import/keycloak-patterns-realm.json @@ -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": [ { "username": "admin-user", diff --git a/keycloak/import/mock-google-realm.json b/keycloak/import/mock-google-realm.json new file mode 100644 index 0000000..8ad4f8f --- /dev/null +++ b/keycloak/import/mock-google-realm.json @@ -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 + } + ] + } + ] +} diff --git a/scripts/configure-google-idp.sh b/scripts/configure-google-idp.sh new file mode 100755 index 0000000..dcbddd9 --- /dev/null +++ b/scripts/configure-google-idp.sh @@ -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" diff --git a/scripts/verify-google-broker-config.sh b/scripts/verify-google-broker-config.sh new file mode 100755 index 0000000..ed4dff4 --- /dev/null +++ b/scripts/verify-google-broker-config.sh @@ -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"