Files
document-haness/.run/keycloak-four-patterns/sources.json
T

1172 lines
74 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"sources": [
{
"id": "AP1_BOUNDARY",
"title": "AP1 SPA direct의 OAuth·token 책임 경계",
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/docs/internal-spa-direct-no-google.md",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"브라우저의 vanilla JavaScript SPA가 public client spa-public로 Authorization Code + PKCE S256을 수행한다.",
"브라우저가 Keycloak access token을 Bearer header에 넣어 Spring Resource Server를 직접 호출하며 server session은 없다.",
"이 패턴의 명시된 선택 이유는 브라우저에서 OAuth와 token 수명주기를 직접 학습하는 데 있다.",
"대안은 refresh token만 server가 보관하는 AP2, 모든 OAuth token을 server가 보관하는 AP3, 인증을 edge로 옮기는 AP4다."
],
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Current branch state reviewed read-only; rationale is scoped to the repository's learning purpose.",
"source_type": "branch-note",
"status": "reviewed",
"path": "docs/internal-spa-direct-no-google.md",
"heading": "AP1 internal SPA direct: local identity profile",
"line_start": 3,
"line_end": 10,
"claim_ids": [
"AP1-C1"
],
"decision_ids": [
"AP1-D1"
],
"priority": 100.0
},
{
"id": "AP1_STORAGE",
"title": "AP1 token 저장 선택과 수용 비용",
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/docs/ap1-token-storage.md",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"access, refresh, ID token은 JavaScript memory에만 두고 redirect transaction state와 PKCE verifier만 sessionStorage에 둔다.",
"persistent token 복사본을 reload 뒤 남기지 않는 대신 reload 생존을 포기한다.",
"memory-only 저장은 실행 중 XSS나 fetch hook이 현재 token 또는 API 권한을 악용하는 것을 막지 못한다.",
"대안인 localStorage·sessionStorage는 reload 편의 대신 persistent script-readable token surface를 늘리고, HttpOnly cookie는 BFF 또는 edge 패턴으로 책임 경계를 바꾼다."
],
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca.",
"source_type": "branch-note",
"status": "reviewed",
"path": "docs/ap1-token-storage.md",
"heading": "AP1 token storage trade-off",
"line_start": 3,
"line_end": 29,
"claim_ids": [
"AP1-C2"
],
"decision_ids": [
"AP1-D2"
],
"priority": 100.0
},
{
"id": "AP1_LOGIN_RUNTIME",
"title": "AP1 SPA authorization, callback와 browser token data flow",
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/frontend/src/app.js",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"SPA UserManager는 spa-public, response_type code, openid profile email scope, callback /callback.html과 in-memory user store를 구성한다.",
"Login click은 signinRedirect를 호출하고 effective authorization request에는 state, PKCE challenge와 S256 method가 포함된다.",
"Callback path에 code 또는 error query가 있으면 signinRedirectCallback이 transaction state와 verifier를 사용해 browser에서 token endpoint로 code를 교환한다.",
"Token response의 access, refresh, ID token은 oidc-client-ts User와 currentUser를 통해 JavaScript memory에 있고 redirect transaction state와 verifier만 sessionStorage를 건넌다.",
"SPA code의 callback은 /callback.html이지만 local realm은 localhost와 127.0.0.1의 port 8088 wildcard redirect를 허용하며 invalid redirect negative test는 없다.",
"Callback 완료 뒤 URL query를 root로 지우고 subject, username, expiry와 token owner를 파생한 metadata만 UI에 표시한다."
],
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Facts reconcile app.js, token storage and PKCE docs, realm configuration, and E2E. Library-internal serialized schema is not claimed.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "frontend/src/app.js",
"heading": "UserManager configuration, callback, and renderSession",
"line_start": 9,
"line_end": 81,
"claim_ids": [
"AP1-C4"
],
"decision_ids": [],
"priority": 100.0
},
{
"id": "AP1_PKCE_DEMO_GAP",
"title": "AP1 manual PKCE helper와 실제 signin path의 구분",
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/frontend/src/pkce.js",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"createPkcePair helper는 32 random bytes를 Base64URL verifier로 만들고 SHA-256 challenge와 S256 method를 반환한다.",
"이 helper는 UI의 PKCE demo button에서 길이를 보여 주는 수동 예시이고 actual signinRedirect path가 호출하지 않는다.",
"실제 login PKCE는 pinned oidc-client-ts library가 수행하므로 demo helper의 verifier 길이를 actual token request의 정확한 library output이라고 주장할 수 없다.",
"E2E는 authorization request의 response_type code, S256 method와 nonempty challenge를 검사하지만 token request verifier 값 자체는 직접 assert하지 않는다."
],
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. This source records an implementation/test evidence boundary.",
"source_type": "canonical-project",
"status": "reviewed-gap",
"path": "frontend/src/pkce.js",
"heading": "createPkcePair",
"line_start": 1,
"line_end": 25,
"claim_ids": [
"AP1-C5"
],
"decision_ids": [],
"priority": 85.0
},
{
"id": "AP1_API_RUNTIME",
"title": "AP1 browser Bearer input에서 /api/me JSON까지",
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/frontend/src/app.js",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Call API click은 currentUser가 없거나 expired이면 network call 없이 login-required UI error를 만들고, 유효하면 absolute http://localhost:8081/api/me에 Bearer access token을 보낸다.",
"실제 SPA happy path는 frontend nginx의 /api proxy가 아니라 browser에서 Resource Server host port를 직접 호출한다.",
"Resource Server는 stateless filter chain에서 Bearer JWT를 Nimbus decoder, issuer and timestamp validator, keycloak-pattern-api audience validator와 realm-role converter로 처리한다.",
"ApiController.currentUser는 verified Jwt를 subject, username, issuer, audience 네 필드 JSON으로 변환한다.",
"SPA는 HTTP status, Resource Server JSON과 browser-memory token metadata를 한 화면용 wrapper JSON으로 다시 조립한다."
],
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Facts reconcile frontend app.js/nginx, backend security/decoder/converter/controller, and E2E.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "frontend/src/app.js",
"heading": "callProtectedApi",
"line_start": 83,
"line_end": 109,
"claim_ids": [
"AP1-C6"
],
"decision_ids": [],
"priority": 100.0
},
{
"id": "AP1_ROLE_FAILURE_RUNTIME",
"title": "AP1 JWT failure와 realm role authorization 경계",
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/backend/src/main/java/com/example/keycloakpattern/KeycloakRealmRoleConverter.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"KeycloakRealmRoleConverter는 realm_access.roles의 string values를 ROLE_ prefixed Spring authorities로 바꾸고 claim이 없으면 empty authority list를 반환한다.",
"/api/me는 authenticated만 요구하므로 valid JWT에 role이 없어도 role converter 결과만으로 거부되지 않으며 admin-role은 /api/admin에서 요구된다.",
"Committed contracts define missing Bearer, wrong audience와 wrong issuer as 401 and regular-user access to /api/admin as 403.",
"Invalid signature와 expired JWT는 전용 E2E negative case가 없고 injected MockMvc jwt success는 Nimbus decoder path를 증명하지 않는다.",
"SPA는 non-2xx 응답에서도 먼저 response.json을 시도하므로 empty or non-JSON 401의 exact failure UX는 고정되지 않았다."
],
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Facts reconcile converter/security/controller, unit and E2E contracts, and frontend error handling.",
"source_type": "canonical-project",
"status": "reviewed-gap",
"path": "backend/src/main/java/com/example/keycloakpattern/KeycloakRealmRoleConverter.java",
"heading": "convert",
"line_start": 12,
"line_end": 27,
"claim_ids": [
"AP1-C7"
],
"decision_ids": [],
"priority": 90.0
},
{
"id": "AP1_GUARDRAILS",
"title": "AP1 public client와 Resource Server 가드레일",
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/keycloak/import/keycloak-patterns-realm.json",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"spa-public client는 public이고 standard flow만 사용하며 implicit와 direct grant를 끄고 PKCE S256을 강제한다.",
"access token에는 keycloak-pattern-api audience가 추가된다.",
"Spring Resource Server는 issuer, timestamp, signature와 audience를 검증하고 realm role을 ROLE_ authority로 변환한다.",
"access token TTL은 300초이며 refresh rotation과 reuse 0 설정을 사용한다.",
"self-contained access token은 logout이나 refresh revocation 뒤에도 만료 전까지 유효할 수 있어 짧은 TTL을 수용한다."
],
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Facts reconcile realm JSON, JwtDecoderConfig, role converter, and ap1-refresh-logout.md.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "keycloak/import/keycloak-patterns-realm.json",
"heading": "spa-public client and realm token settings",
"line_start": 11,
"line_end": 73,
"claim_ids": [
"AP1-C3"
],
"decision_ids": [],
"priority": 85.0
},
{
"id": "AP1_VERIFY",
"title": "AP1 브라우저 흐름과 token 수명주기 검증 계약",
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/e2e/pattern1.mjs",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Playwright E2E는 authorization request의 PKCE S256, 보호 API 200, wrong audience와 wrong issuer 401을 검사한다.",
"E2E는 실행 중 fetch hook이 Bearer token을 관찰할 수 있음을 재현하고 Web Storage에 access token이 남지 않는 것을 확인한다.",
"refresh token rotation과 이전 refresh token 거부, revocation 뒤 refresh 거부, 이미 발급된 access JWT의 만료 전 유효성을 검사한다.",
"검증 코드는 존재하지만 이번 문서 조사에서는 파괴적인 volume 초기화를 포함한 verify-pattern1.sh를 실행하지 않았다."
],
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Test-defined evidence, not a fresh execution result.",
"source_type": "canonical-project",
"status": "test-defined",
"path": "e2e/pattern1.mjs",
"heading": "AP1 Playwright acceptance contract",
"line_start": 82,
"line_end": 202,
"claim_ids": [
"AP1-T1"
],
"decision_ids": [],
"priority": 80.0
},
{
"id": "AP2_BOUNDARY",
"title": "AP2 confidential token mediator의 책임 경계",
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/docs/ap2-token-boundary.md",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"브라우저는 Spring mediator에서 로그인을 시작하고 confidential client인 mediator가 client secret으로 authorization code를 교환한다.",
"mediator는 access와 refresh token을 OAuth2AuthorizedClientService에 보관한다.",
"브라우저가 token endpoint를 호출하면 mediator는 현재 access token, token type, 만료 시각만 no-store 응답으로 전달한다.",
"브라우저는 전달받은 access token을 memory에서 사용해 Resource Server를 직접 Bearer 방식으로 호출하며 refresh token은 받지 않는다.",
"선택 이유는 브라우저에서 code 교환과 refresh token을 제거하면서 Bearer 중심 API 호출은 유지하는 데 있다."
],
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff.",
"source_type": "branch-note",
"status": "reviewed",
"path": "docs/ap2-token-boundary.md",
"heading": "책임 경계",
"line_start": 3,
"line_end": 18,
"claim_ids": [
"AP2-C1"
],
"decision_ids": [
"AP2-D1"
],
"priority": 100.0
},
{
"id": "AP2_IMPLEMENTATION",
"title": "AP2 access-only handoff의 실제 구현",
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/token-mediator/src/main/java/com/example/keycloakpattern/mediator/AccessTokenController.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"GET /token/access는 Keycloak access token 원문, token type, expires_at을 반환한다.",
"응답에는 Cache-Control no-store와 Pragma no-cache가 붙고 refresh token 필드는 없다.",
"반복 호출을 막는 nonce, consume, delete 로직은 구현되어 있지 않다.",
"따라서 현재 branch를 one-time handoff code 구현이라고 설명할 수 없고 access-only token handoff라고 좁혀야 한다."
],
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. This implementation takes precedence over the broader wording in the common trade-off matrix.",
"source_type": "canonical-project",
"status": "reviewed-discrepancy",
"path": "token-mediator/src/main/java/com/example/keycloakpattern/mediator/AccessTokenController.java",
"heading": "accessToken",
"line_start": 28,
"line_end": 55,
"claim_ids": [
"AP2-C2"
],
"decision_ids": [],
"priority": 100.0
},
{
"id": "AP2_LOGIN_FLOW",
"title": "AP2 browser entry와 Spring oauth2Login code 교환",
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/token-mediator/src/main/resources/static/app.js",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Login button은 browser를 /oauth2/authorization/keycloak로 이동시키며 Spring Security가 token-mediating-confidential client의 authorization request를 시작한다.",
"Spring Security는 authorization request와 state를 HttpSession에 저장하고 AP2_SESSION으로 callback transaction을 연결한 뒤 authenticated SecurityContext를 같은 session 경계에 둔다.",
"Keycloak callback은 /login/oauth2/code/keycloak이고 token endpoint의 client authentication method는 client_secret_basic이다.",
"Spring oauth2Login이 code를 server-to-server로 교환하고 성공 뒤 root URL로 돌려보낸다.",
"AP2 client 설정에는 PKCE S256 강제 속성이 없고 E2E도 AP2 authorization request의 challenge를 검사하지 않는다.",
"AP2_SESSION은 OAuth token 값이 아니라 server login state를 찾는 HttpOnly SameSite=Lax session cookie다."
],
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. Facts reconcile static app.js, SecurityConfig, application.yml, realm JSON, and pattern2 E2E.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "token-mediator/src/main/resources/static/app.js",
"heading": "loginButton click and OAuth client registration",
"line_start": 7,
"line_end": 9,
"claim_ids": [
"AP2-C4"
],
"decision_ids": [],
"priority": 90.0
},
{
"id": "AP2_BOUNDARY_RUNTIME",
"title": "AP2 token boundary endpoint input과 output",
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/token-mediator/src/main/java/com/example/keycloakpattern/mediator/TokenBoundaryController.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"GET /token/boundary는 AP2_SESSION으로 인증된 principal을 입력으로 받고 registration ID keycloak과 principal name으로 authorized client를 조회한다.",
"성공 응답은 pattern, principal, accessTokenStored, refreshTokenStored, browserReceivesRefreshToken의 다섯 필드이며 no-store와 no-cache를 사용한다.",
"Authorized client가 없더라도 endpoint는 token 보관 boolean을 false로 둔 200 상태 진단 응답을 만들며 token 부재 자체를 실패로 강제하지 않는다.",
"Preferred username을 principal name으로 쓰도록 client provider가 설정되어 local regular-user login의 principal 값은 regular-user로 구성된다."
],
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. Exact payload shape reconciled with TokenBoundaryControllerTest.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "token-mediator/src/main/java/com/example/keycloakpattern/mediator/TokenBoundaryController.java",
"heading": "tokenBoundary",
"line_start": 25,
"line_end": 42,
"claim_ids": [
"AP2-C5"
],
"decision_ids": [],
"priority": 95.0
},
{
"id": "AP2_ACCESS_RUNTIME",
"title": "AP2 access handoff와 browser direct API의 data transformation",
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/token-mediator/src/main/java/com/example/keycloakpattern/mediator/AccessTokenController.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"GET /token/access는 OAuth2AuthorizeRequest에 registration ID keycloak과 현재 Authentication을 넣고 OAuth2AuthorizedClientManager.authorize를 호출한다.",
"성공 응답의 정확한 키 집합은 access_token, token_type, expires_at이며 raw Keycloak JWT가 access_token 값으로 browser에 전달된다.",
"Authorized client 또는 access token이 없으면 controller는 401과 No authorized Keycloak client is available reason을 만든다; 정확한 Spring error body는 별도로 고정되지 않았다.",
"JavaScript는 access_token을 지역 변수로 읽어 http://localhost:8081/api/me의 Authorization Bearer header로 즉시 변환하며 persistent Web Storage에 쓰지 않는다.",
"현재 controller는 매 GET마다 현재 access token을 반환하고 nonce, consume flag, delete 또는 replay rejection을 구현하지 않는다."
],
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. Facts reconcile AccessTokenController, static app.js, and tests.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "token-mediator/src/main/java/com/example/keycloakpattern/mediator/AccessTokenController.java",
"heading": "accessToken",
"line_start": 28,
"line_end": 54,
"claim_ids": [
"AP2-C6"
],
"decision_ids": [],
"priority": 100.0
},
{
"id": "AP2_RESOURCE_RUNTIME",
"title": "AP2 Resource Server의 JWT input과 /api/me output",
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/backend/src/main/java/com/example/keycloakpattern/ApiController.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"브라우저는 AP2 UI origin에서 GET /api/me에 Accept application/json과 Authorization Bearer access token을 보낸다.",
"Resource Server는 stateless로 signature, issuer, timestamp와 keycloak-pattern-api audience를 검증한다.",
"ApiController.currentUser는 검증된 Jwt를 입력으로 subject, username, issuer, audience 네 필드의 JSON을 반환한다.",
"AP2 UI origin에는 /api/**의 GET과 OPTIONS 및 Authorization과 Content-Type header만 허용하도록 CORS가 설정된다.",
"커밋된 E2E는 실제 Keycloak JWT로 status 200, regular-user username과 expected audience를 검사하도록 정의하지만 이번 조사에서 실행하지 않았다."
],
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. Facts reconcile backend controller, security/decoder/validator configuration, and E2E.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "backend/src/main/java/com/example/keycloakpattern/ApiController.java",
"heading": "currentUser",
"line_start": 21,
"line_end": 28,
"claim_ids": [
"AP2-C7"
],
"decision_ids": [],
"priority": 95.0
},
{
"id": "AP2_GUARDRAILS",
"title": "AP2 session, refresh custody, CORS와 audience 가드레일",
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/token-mediator/src/main/resources/application.yml",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"AP2_SESSION은 HttpOnly와 SameSite=Lax를 사용하고 실제 OAuth token을 cookie 안에 넣지 않는다.",
"client_secret_basic confidential client와 environment-provided secret을 사용한다.",
"downstream API는 AP2 UI origin의 GET과 OPTIONS만 CORS로 허용하고 stateless JWT Resource Server로 동작한다.",
"access token 노출은 남고 mediator session과 authorized-client 상태가 추가되므로 AP1보다 수평 확장이 복잡하다.",
"durable shared authorized-client store, logout, refresh 이후 동작은 현재 branch에 구현·검증 근거가 없다."
],
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. The scaling cost is an implementation-grounded inference, not a recorded project ADR.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "token-mediator/src/main/resources/application.yml",
"heading": "AP2 session and OAuth client configuration",
"line_start": 3,
"line_end": 34,
"claim_ids": [
"AP2-C3"
],
"decision_ids": [],
"priority": 85.0
},
{
"id": "AP2_VERIFY",
"title": "AP2 access-only 전달과 브라우저 직접 API 호출 검증 계약",
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/e2e/pattern2.mjs",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Playwright E2E는 server에 access와 refresh token이 있고 browser 응답에는 refresh token이 없음을 검사한다.",
"access 응답이 정확히 access_token, expires_at, token_type 세 필드이고 no-store인지 검사한다.",
"access token audience와 직접 Resource Server 호출 200, AP2_SESSION의 HttpOnly와 SameSite=Lax, Web Storage 비사용을 검사한다.",
"검증 코드는 존재하지만 이번 조사에서는 verify-pattern2.sh를 새로 실행하지 않았다."
],
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. Test-defined evidence, not a fresh execution result.",
"source_type": "canonical-project",
"status": "test-defined",
"path": "e2e/pattern2.mjs",
"heading": "AP2 Playwright acceptance contract",
"line_start": 39,
"line_end": 112,
"claim_ids": [
"AP2-T1"
],
"decision_ids": [],
"priority": 80.0
},
{
"id": "AP3_BOUNDARY",
"title": "AP3 BFF의 tokenless browser 경계",
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/docs/ap3-bff-boundary.md",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"BFF가 confidential client와 PKCE S256으로 authorization code를 교환하고 access와 refresh token을 server에 보관한다.",
"브라우저에는 OAuth token 대신 HttpOnly AP3_SESSION만 남는다.",
"브라우저가 BFF API를 cookie로 호출하면 BFF가 Bearer access token을 붙여 내부 Resource Server를 호출한다.",
"선택 이유는 브라우저에서 OAuth token을 제거하고 application authorization과 session을 중앙화하는 데 있다.",
"대안 AP1은 stateless와 protocol transparency를 얻고, AP2는 access token 직접 전달을 유지하며, AP4는 edge에서 기존 upstream을 보호한다."
],
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677.",
"source_type": "branch-note",
"status": "reviewed",
"path": "docs/ap3-bff-boundary.md",
"heading": "요청과 token 경계",
"line_start": 3,
"line_end": 18,
"claim_ids": [
"AP3-C1"
],
"decision_ids": [
"AP3-D1"
],
"priority": 100.0
},
{
"id": "AP3_TRADEOFF",
"title": "AP3와 AP1의 위협 모델·운영비 교환",
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/docs/bff-vs-spa-direct.md",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"BFF는 browser JavaScript가 token을 읽지 못하게 하지만 XSS가 same-origin request를 악용하는 것까지 없애지는 않는다.",
"cookie session으로 바뀌므로 CSRF 방어가 필요하고 backend session store가 필요하다.",
"학습 구성은 session과 authorized client를 단일 instance memory에 두므로 재시작 시 session이 사라진다.",
"scale-out에는 sticky session 또는 Spring Session과 Redis 같은 shared store, 저장 token 암호화 정책이 필요하다.",
"BFF 선택은 절대적인 보안 등급이 아니라 browser token 비노출과 stateful 운영비의 교환이다."
],
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677.",
"source_type": "branch-note",
"status": "reviewed",
"path": "docs/bff-vs-spa-direct.md",
"heading": "BFF vs SPA direct",
"line_start": 3,
"line_end": 22,
"claim_ids": [
"AP3-C2"
],
"decision_ids": [
"AP3-D2"
],
"priority": 100.0
},
{
"id": "AP3_GUARDRAILS",
"title": "AP3 CSRF token과 SameSite 가드레일",
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Spring CookieCsrfTokenRepository가 JavaScript-readable XSRF-TOKEN을 발급하고 client는 X-XSRF-TOKEN header를 보낸다.",
"AP3_SESSION은 HttpOnly와 SameSite=Lax다.",
"SameSite는 CSRF token의 대체가 아니라 defense-in-depth다.",
"BFF는 access와 refresh token 값을 browser 응답에 넣지 않고 BFF가 downstream Bearer header를 만든다."
],
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. Facts reconcile SecurityConfig, application.yml, and BffController.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java",
"heading": "bffSecurity",
"line_start": 25,
"line_end": 58,
"claim_ids": [
"AP3-C3"
],
"decision_ids": [],
"priority": 85.0
},
{
"id": "AP3_LOGIN_FLOW",
"title": "AP3 oauth2Login과 server-side PKCE data flow",
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Login button은 /oauth2/authorization/keycloak로 이동하고 SecurityConfig의 DefaultOAuth2AuthorizationRequestResolver가 withPkce customizer로 state, verifier와 S256 challenge를 준비한다.",
"Authorization request는 bff-confidential, response_type code, callback /login/oauth2/code/keycloak, openid profile email scope와 PKCE S256 challenge를 사용한다.",
"Callback 뒤 BFF가 client_secret_basic, authorization code와 verifier로 server-to-server token 교환을 수행하고 access와 refresh token은 authorized-client service에 저장하며 ID token에서 구성된 OIDC principal은 HttpSession SecurityContext에 연결한다.",
"브라우저에는 OAuth token 대신 HttpOnly SameSite=Lax AP3_SESSION이 남고 성공 뒤 root URL로 이동한다.",
"현재 application에는 Spring Session, Redis, JDBC authorized-client store 의존성이 없어 session과 token state는 single-process memory 경계다."
],
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. Framework-mediated steps are reconciled with SecurityConfig, application.yml, realm config, E2E, and Spring defaults.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java",
"heading": "authorizationRequestResolver and bffSecurity",
"line_start": 20,
"line_end": 58,
"claim_ids": [
"AP3-C4"
],
"decision_ids": [],
"priority": 95.0
},
{
"id": "AP3_BOUNDARY_RUNTIME",
"title": "AP3 token boundary endpoint의 input과 관측 output",
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"GET /bff/token-boundary는 AP3_SESSION으로 복원된 Authentication을 입력으로 받고 keycloak registration과 principal name으로 authorized client를 직접 조회한다.",
"정상 응답은 pattern, principal, accessTokenStoredOnServer, refreshTokenStoredOnServer, browserTokenCount, csrfProtectionEnabled 여섯 필드이며 no-store와 no-cache를 사용한다.",
"browserTokenCount 값 0은 controller의 literal 진단 필드이고 실제 browser를 측정한 값은 아니므로 E2E의 storage와 network 검사가 별도로 필요하다.",
"Authorized client가 없더라도 인증된 요청이면 server token 보관 boolean이 false인 200 진단 응답을 만들며 access나 refresh token 원문은 직렬화하지 않는다."
],
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
"heading": "tokenBoundary",
"line_start": 44,
"line_end": 64,
"claim_ids": [
"AP3-C5"
],
"decision_ids": [],
"priority": 95.0
},
{
"id": "AP3_API_RUNTIME",
"title": "AP3 session input에서 downstream Bearer와 reader JSON까지",
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"GET /bff/api/me는 browser Authorization header 없이 AP3_SESSION으로 들어오며 BffController.currentUser가 현재 Authentication을 받는다.",
"authorizedClient helper는 OAuth2AuthorizeRequest를 만들고 refresh-token-capable OAuth2AuthorizedClientManager.authorize를 호출해 현재 access token을 얻는다.",
"BFF RestClient는 internal Resource Server GET /api/me에 server-held access token을 Bearer header로 붙이고 browser session cookie는 전달하지 않는다.",
"Resource Server는 JWT signature, issuer, timestamp와 keycloak-pattern-api audience를 검증하고 subject, username, issuer, audience JSON을 반환한다.",
"BFF는 downstream ResponseEntity Map을 반환하지만 downstream 401, timeout과 unavailable을 명시적으로 그대로 매핑하거나 retry하는 계약은 구현하지 않는다."
],
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. Facts reconcile BffController, manager bean, backend JWT configuration, and E2E.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
"heading": "currentUser and authorizedClient",
"line_start": 67,
"line_end": 111,
"claim_ids": [
"AP3-C6"
],
"decision_ids": [],
"priority": 100.0
},
{
"id": "AP3_CSRF_RUNTIME",
"title": "AP3 CSRF cookie-to-header transformation과 preferences output",
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/CsrfController.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"GET /bff/csrf는 authenticated session을 입력으로 받고 headerName, parameterName, token JSON과 JavaScript-readable XSRF-TOKEN cookie를 no-store로 반환한다.",
"JSON body의 token은 XOR-masked request attribute token이고 XSRF-TOKEN cookie에는 raw token이 있으므로 두 문자열을 동일하다고 설명할 수 없다.",
"SPA는 JSON의 headerName을 읽고 document.cookie의 raw XSRF-TOKEN 값을 X-XSRF-TOKEN request header에 넣는다.",
"SpaCsrfTokenRequestHandler는 token attribute 노출에는 XOR handler를 사용하지만 expected header가 있으면 plain resolver로 submitted raw token을 읽는다.",
"POST /bff/api/preferences는 AP3_SESSION, XSRF-TOKEN cookie, matching X-XSRF-TOKEN header와 form theme를 입력으로 받으며 header가 없거나 틀리면 controller 전에 403이다.",
"정상 POST는 updated, theme, principal JSON을 반환한다; SameSite=Lax는 별도 방어선이며 CSRF token 검증을 대체하지 않는다."
],
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. Facts reconcile CsrfController, SecurityConfig, SpaCsrfTokenRequestHandler, app.js, BffController, and tests.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "bff/src/main/java/com/example/keycloakpattern/bff/CsrfController.java",
"heading": "csrf",
"line_start": 14,
"line_end": 23,
"claim_ids": [
"AP3-C7"
],
"decision_ids": [],
"priority": 100.0
},
{
"id": "AP3_PREFERENCE_SCOPE",
"title": "AP3 preferences 예시의 process-global state 간극",
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Preference theme은 singleton controller의 AtomicReference String 한 개에 저장되고 user 또는 session key가 없다.",
"한 사용자의 update가 process 안의 다른 사용자 조회에도 보일 수 있고 재시작하면 system으로 초기화된다.",
"AtomicReference는 set과 get 원자성만 제공하며 사용자 격리, 입력 validation, persistence, audit 또는 authorization을 제공하지 않는다.",
"현재 POST는 arbitrary theme string을 받아 인증된 principal만 응답에 기록하고 role 또는 ownership을 검사하지 않는다."
],
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. This is an implementation-grounded scope warning for the worked example.",
"source_type": "canonical-project",
"status": "reviewed-gap",
"path": "bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
"heading": "preferenceTheme and updatePreferences",
"line_start": 28,
"line_end": 95,
"claim_ids": [
"AP3-C8"
],
"decision_ids": [],
"priority": 90.0
},
{
"id": "AP3_VERIFY",
"title": "AP3 browser token 비노출과 CSRF 방어 검증 계약",
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/e2e/pattern3.mjs",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Playwright E2E는 PKCE S256, server token 보관, browser token count 0, token endpoint와 Resource Server 직접 호출 부재를 검사한다.",
"AP3_SESSION의 HttpOnly와 SameSite=Lax, 빈 Web Storage를 검사한다.",
"CSRF token 없는 POST 403, 올바른 header가 있는 POST 200, cross-site POST에서 session cookie 제외를 검사한다.",
"검증 코드는 존재하지만 이번 조사에서는 verify-pattern3.sh를 새로 실행하지 않았다."
],
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. Test-defined evidence, not a fresh execution result.",
"source_type": "canonical-project",
"status": "test-defined",
"path": "e2e/pattern3.mjs",
"heading": "AP3 Playwright acceptance contract",
"line_start": 43,
"line_end": 191,
"claim_ids": [
"AP3-T1"
],
"decision_ids": [],
"priority": 80.0
},
{
"id": "AP4_BOUNDARY",
"title": "AP4 oauth2-proxy와 nginx edge 책임 경계",
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/docs/ap4-edge-forward-auth.md",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"oauth2-proxy가 confidential edge-proxy client와 PKCE S256으로 code를 교환하고 browser에는 HttpOnly AP4_SESSION만 남긴다.",
"nginx auth_request가 oauth2-proxy의 인증 결과를 확인하고 허용된 identity header만 upstream application에 전달한다.",
"선택 이유는 OAuth와 OIDC를 모르는 기존 upstream을 수정하기 어려울 때 edge에서 인증을 일괄 적용하는 데 있다.",
"수용 비용은 proxy session 운영과 identity header 신뢰 경계를 네트워크·application 양쪽에서 강제해야 한다는 점이다.",
"대안은 application-owned session과 authorization을 제공하는 AP3 또는 Traefik ForwardAuth 같은 다른 edge policy point다.",
"최종 hardened 구현은 upstream에 internal token 검증을 요구하므로 완전한 무수정 통합이 아니라 OAuth 비인지 application에 최소 신뢰경계 통합을 추가하는 형태다."
],
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563.",
"source_type": "branch-note",
"status": "reviewed",
"path": "docs/ap4-edge-forward-auth.md",
"heading": "AP4 oauth2-proxy Edge Forward Auth",
"line_start": 3,
"line_end": 70,
"claim_ids": [
"AP4-C1"
],
"decision_ids": [
"AP4-D1"
],
"priority": 100.0
},
{
"id": "AP4_NGINX",
"title": "AP4 auth_request와 identity header 덮어쓰기",
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/frontend/default.conf.template",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"정확 일치 /oauth2/auth location은 internal이고 auth subrequest body를 전달하지 않는다.",
"일반 browser 요청의 401은 login 302로 바꾸지만 /api/edge는 redirect 없이 JSON 401을 반환한다.",
"client가 보낸 identity와 internal token header는 사용하지 않고 oauth2-proxy 결과와 server-side internal token으로 덮어쓴다.",
"backend와 oauth2-proxy port는 host에 publish하지 않고 nginx만 application entry point로 노출한다.",
"학습용 nginx 예제는 보호 경로를 범용 upstream path로 보존하지 않고 /edge/me로 전달하므로 identity flow fixture이지 완성형 transparent reverse proxy가 아니다."
],
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Facts reconcile nginx template and docker-compose.yml.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "frontend/default.conf.template",
"heading": "nginx AP4 server configuration",
"line_start": 13,
"line_end": 74,
"claim_ids": [
"AP4-C2"
],
"decision_ids": [],
"priority": 90.0
},
{
"id": "AP4_LOGIN_RUNTIME",
"title": "AP4 unauthenticated navigation에서 oauth2-proxy session까지",
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/docker-compose.yml",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Cookie가 없는 GET /는 nginx auth_request를 통해 internal /oauth2/auth를 조회하고 oauth2-proxy 401을 /oauth2/start redirect로 변환한다.",
"oauth2-proxy는 edge-proxy confidential client, S256 challenge, browser-facing login URL, server-facing token/JWKS/userinfo URL과 callback /oauth2/callback을 사용한다.",
"Callback code 교환은 oauth2-proxy와 Keycloak 사이의 server-to-server 요청이고 browser request log에는 token endpoint call이 없어야 한다.",
"로그인 뒤 browser에는 HttpOnly SameSite=Lax AP4_SESSION이 남으며 local HTTP fixture는 Secure false이고 production HTTPS에서는 secure cookie가 필요하다.",
"별도 Redis 같은 server-side session store는 없고 session-cookie-minimal은 client-side cookie에 access, refresh, ID token을 보관하지 않으므로 persistent refresh-token custody나 refresh lifecycle은 구현·검증되지 않았다."
],
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Facts reconcile Compose flags, nginx template, docs/ap4-edge-forward-auth.md, docs/edge-forwardauth-google-federation.md, and E2E.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "docker-compose.yml",
"heading": "oauth2-proxy service",
"line_start": 92,
"line_end": 143,
"claim_ids": [
"AP4-C5"
],
"decision_ids": [],
"priority": 100.0
},
{
"id": "AP4_REQUEST_RUNTIME",
"title": "AP4 external request에서 auth subrequest와 upstream input까지",
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/frontend/default.conf.template",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Authenticated GET /api/edge는 먼저 body 없는 internal /oauth2/auth subrequest로 변환되고 original URL, forwarded host, protocol, URI와 client address context가 oauth2-proxy에 전달된다.",
"Nginx는 oauth2-proxy response의 X-Auth-Request-User, X-Auth-Request-Email과 Set-Cookie를 추출한다.",
"원래 external /api/edge URL은 upstream GET /edge/me로 다시 매핑되고 client-supplied identity/internal headers는 extracted user/email과 server-side internal token으로 덮어쓴다.",
"Unauthenticated exact /api/edge는 redirect 없이 401 JSON error authentication required를 반환하지만 general / location은 login 302로 바뀐다.",
"External /oauth2/auth는 internal location 때문에 접근할 수 없고 current example maps protected routes to one identity endpoint rather than preserving arbitrary upstream paths."
],
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Exact nginx behavior, not a generic forward-auth claim.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "frontend/default.conf.template",
"heading": "auth_request and upstream mapping",
"line_start": 13,
"line_end": 74,
"claim_ids": [
"AP4-C6"
],
"decision_ids": [],
"priority": 100.0
},
{
"id": "AP4_RESPONSE_RUNTIME",
"title": "AP4 trusted header input에서 edge identity JSON까지",
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/backend/src/main/java/com/example/keycloakpattern/EdgeIdentityController.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"EdgeIdentityController.currentUser는 X-Auth-Request-User를 읽고 X-Internal-Auth-Token을 configured bytes와 MessageDigest.isEqual로 비교한다.",
"User header가 blank이거나 internal token이 없거나 틀리면 401과 error trusted edge authentication is required JSON을 반환한다.",
"정상 응답은 pattern AP4-edge-forward-auth, user, email, identityHeader X-Auth-Request-User 네 필드다.",
"Spring Security는 /edge/**를 permitAll로 두므로 current internal-token check는 /edge/me controller의 local guard이며 모든 edge endpoint의 centralized filter가 아니다.",
"현재 response와 test는 user/email identity만 다루고 role, groups, tenant 또는 fine-grained authorization contract를 구현하지 않는다."
],
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Facts reconcile controller, SecurityConfig, unit tests, and E2E.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "backend/src/main/java/com/example/keycloakpattern/EdgeIdentityController.java",
"heading": "currentUser and hasValidInternalToken",
"line_start": 27,
"line_end": 53,
"claim_ids": [
"AP4-C7"
],
"decision_ids": [],
"priority": 100.0
},
{
"id": "AP4_BACKEND",
"title": "AP4 upstream의 내부 token 검증",
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/backend/src/main/java/com/example/keycloakpattern/EdgeIdentityController.java",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"upstream endpoint는 X-Auth-Request-User와 X-Internal-Auth-Token이 모두 있어야 identity를 받아들인다.",
"internal token은 MessageDigest.isEqual로 비교하며 누락되거나 틀리면 401을 반환한다.",
"shared token은 defense-in-depth이고 production에서는 secret manager 주입·rotation 또는 mTLS와 workload identity가 더 강한 대안이다.",
"현재 upstream은 user와 email만 소비하며 role header 또는 application authorization 전달은 구현·검증하지 않는다."
],
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Secret lifecycle guidance is from docs/ap4-edge-forward-auth.md lines 67-70.",
"source_type": "canonical-project",
"status": "reviewed",
"path": "backend/src/main/java/com/example/keycloakpattern/EdgeIdentityController.java",
"heading": "currentUser and hasValidInternalToken",
"line_start": 18,
"line_end": 53,
"claim_ids": [
"AP4-C3"
],
"decision_ids": [],
"priority": 85.0
},
{
"id": "AP4_VERIFY",
"title": "AP4 edge login과 header spoofing 방어 검증 계약",
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/e2e/pattern4.mjs",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Playwright E2E는 unauthenticated 302, PKCE S256, server-to-server token 교환, HttpOnly SameSite=Lax AP4_SESSION을 검사한다.",
"공격자가 identity와 internal token header를 보내도 nginx가 덮어써 authenticated user가 바뀌지 않는지 검사한다.",
"외부 /oauth2/auth 접근은 404, API unauthenticated 요청은 redirect 없는 401, oauth2-proxy와 backend host port는 접근 불가인지 검사한다.",
"검증 코드는 존재하지만 이번 조사에서는 volume을 삭제하는 verify-pattern4.sh를 새로 실행하지 않았다."
],
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Test-defined evidence, not a fresh execution result.",
"source_type": "canonical-project",
"status": "test-defined",
"path": "e2e/pattern4.mjs",
"heading": "AP4 Playwright acceptance contract",
"line_start": 44,
"line_end": 131,
"claim_ids": [
"AP4-T1"
],
"decision_ids": [],
"priority": 80.0
},
{
"id": "AP4_ALTERNATIVE",
"title": "AP4 Traefik ForwardAuth 대안과 추가 비용",
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/docs/traefik-forwardauth-alternative.md",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"Traefik forwardAuth도 oauth2-proxy /oauth2/auth를 policy point로 사용할 수 있지만 OIDC client나 session manager 자체는 아니다.",
"trustForwardHeader=false와 허용 identity header 복사가 필요하다.",
"nginx의 error_page와 같은 login redirect UX는 별도 middleware 또는 oauth2-proxy profile을 설계해야 한다.",
"repository baseline은 학습 가시성이 높은 nginx 조합을 유지하고 Traefik은 configuration-load 수준의 대안으로만 검증한다.",
"Traefik 예제는 hardened backend가 요구하는 X-Internal-Auth-Token을 주입하지 않아 현재 /edge/me를 그대로 통과하는 drop-in 대안으로 입증되지 않았다."
],
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563.",
"source_type": "branch-note",
"status": "config-tested",
"path": "docs/traefik-forwardauth-alternative.md",
"heading": "Traefik ForwardAuth alternative",
"line_start": 3,
"line_end": 22,
"claim_ids": [
"AP4-C4"
],
"decision_ids": [
"AP4-D2"
],
"priority": 85.0
},
{
"id": "BRANCH_REACHABILITY",
"title": "네 pattern branch와 39개 feature ref의 도달성",
"url": "repo://keycloak-pattern/develop/docs/keycloak-branch-manifest.tsv",
"publisher": "keycloak-pattern Git repository",
"accessed": "",
"facts": [
"manifest에는 common, ap1, ap2, ap3, ap4 target으로 분류된 39개 feature branch가 있다.",
"읽기 전용 Git 검사에서 origin의 39개 feature ref가 모두 존재하고 선언된 develop 또는 pattern branch tip의 ancestor임을 확인했다.",
"repository audit script 자체는 현재 로컬에 없는 별도 branch-note inventory 경로를 요구해 이번 환경에서는 완료되지 않았다."
],
"notes": "Read-only audit on develop@c07593c47144674b35e1a2fc3f2f7cfdb349f683. Remote refs were accepted because local feature refs are not present. This is internal execution evidence.",
"source_type": "canonical-project",
"status": "partially-verified",
"path": "docs/keycloak-branch-manifest.tsv",
"heading": "branch target delivery registry",
"line_start": 1,
"line_end": 40,
"claim_ids": [
"COMMON-T1"
],
"decision_ids": [],
"priority": 70.0
},
{
"id": "L4121b8d86b",
"title": "four pattern tradeoff matrix — Four Keycloak integration patterns",
"url": "repo:///docs/four-pattern-tradeoff-matrix.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"# Four Keycloak integration patterns\n\n| 축 | AP1 SPA direct | AP2 token mediator | AP3 BFF | AP4 edge auth |\n|---|---|---|---|---|\n| OAuth client | public | confidential | confidential | confidential proxy |\n| browser 보유물 | access/refresh token | 짧은 handoff code 또는 app token | HttpOnly session cookie | proxy session cookie |\n| OAuth code 교환 | browser + PKCE | mediator backend | BFF | oauth2-proxy |\n| API bearer 검증 | Spring resource server | mediator/downstream API | BFF 내부 또는 downstream | edge가 인증 후 trusted header |\n| server session | 없음 | handoff 상태만 짧게 | 필수 | proxy cookie/session |\n| XSS token 탈취면 | 가장 큼 | 축소 | browser token 제거 | browser token 제거 |\n| CSRF 주의 | token endpoint/refresh 설계 | app cookie 사용 시 | 필수 방어 | proxy cookie 사용 시 |\n| 수평 확장 상태 | 단순 | handoff store 공유 가능 | session store 필요 | proxy 설정에 따름 |\n| 주 학습 포인트 | PKCE/JWT/RS | token 경계·one-time handoff | oauth2Login/session/CSRF | auth_request/header trust |"
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/four-pattern-tradeoff-matrix.md",
"heading": "Four Keycloak integration patterns",
"line_start": 1,
"line_end": 14,
"claim_ids": [],
"decision_ids": [],
"priority": 45.74042
},
{
"id": "La5d0a70f24",
"title": "four pattern tradeoff matrix — 이 repository의 실행 증거",
"url": "repo:///docs/four-pattern-tradeoff-matrix.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"## 이 repository의 실행 증거\n\n- AP1: PKCE SPA, issuer/audience, token storage, refresh/logout 검증\n- AP2: confidential client와 one-time access handoff 검증\n- AP3: `oauth2Login` session과 CSRF/SameSite 검증\n- AP4: oauth2-proxy, nginx `auth_request`, spoofed header 제거 검증\n- 공통: local mock Google brokering, First Broker Login, claim/role mapping 검증\n\n각 근거 브랜치와 병합 여부는 `keycloak-branch-manifest.tsv` 및\n`audit-keycloak-branches.sh`로 추적한다."
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/four-pattern-tradeoff-matrix.md",
"heading": "이 repository의 실행 증거",
"line_start": 28,
"line_end": 37,
"claim_ids": [],
"decision_ids": [],
"priority": 21.712857
},
{
"id": "L2c120c8093",
"title": "four pattern tradeoff matrix — 선택 기준",
"url": "repo:///docs/four-pattern-tradeoff-matrix.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"## 선택 기준\n\n- 브라우저에서 OAuth와 token 수명주기를 직접 학습하려면 AP1.\n- 브라우저에 upstream token을 주지 않되 API 호출은 bearer 중심으로 유지하려면\n AP2.\n- token을 browser에서 완전히 제거하고 애플리케이션 단위 인가·세션을\n 중앙화하려면 AP3.\n- 기존 upstream을 수정하기 어렵고 경계에서 일괄 인증하려면 AP4.\n\nGoogle federation은 다섯 번째 인증 패턴이 아니다. 네 패턴 모두 최종적으로\nKeycloak token/session을 소비하며, Google은 Keycloak 앞의 upstream IdP\nhop으로 추가된다."
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/four-pattern-tradeoff-matrix.md",
"heading": "선택 기준",
"line_start": 15,
"line_end": 27,
"claim_ids": [],
"decision_ids": [],
"priority": 17.280962
},
{
"id": "L4ec23ba045",
"title": "keycloak branch index — Keycloak branch implementation index",
"url": "repo:///docs/keycloak-branch-index.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"# Keycloak branch implementation index\n\nThe source inventory contains 39 `feature-keycloak-*.md` branch notes. This\nrepository preserves one local Git feature branch for every note and merges it\nwith `--no-ff` into either the common `develop` baseline or one of the four\nauthentication-pattern branches.\n\n| Target | Meaning |\n|---|---|\n| `common` | Shared realm, federation, deployment, or governance contract. Merge into `develop`, then propagate to AP1AP4. |\n| `ap1` | Browser-based OAuth client: vanilla SPA, Authorization Code + PKCE, Resource Server. |\n| `ap2` | Token-mediating confidential backend: browser receives access token only. |\n| `ap3` | BFF: backend owns every OAuth token and browser owns only a session cookie. |\n| `ap4` | Edge forward-auth: oauth2-proxy/Nginx owns login and backend trusts an isolated identity header. |\n\nThe machine-readable registry is\n[`keycloak-branch-manifest.tsv`](keycloak-branch-manifest.tsv). Run:\n\n```bash\n./scripts/audit-keycloak-branches.sh\n```\n\nThe audit succeeds only when all 39 note names have matching local feature\nbranches and each feature tip is reachable from its declared target branch.\n\nGoogle credentials are never committed. The default local acceptance harness\nuses a second Keycloak realm as a controllable OIDC provider so claim mapping\nand unsafe-linking failure paths can be reproduced. A real Google login remains\nan explicit credentialed/public-HTTPS verification profile."
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/keycloak-branch-index.md",
"heading": "Keycloak branch implementation index",
"line_start": 1,
"line_end": 29,
"claim_ids": [],
"decision_ids": [],
"priority": 14.830096
},
{
"id": "Lb39734ea9b",
"title": "google idp brokering — Google IdP brokering",
"url": "repo:///docs/google-idp-brokering.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"# Google IdP brokering\n\nKeycloak is the only issuer trusted by AP1AP4. Google is an upstream Identity\nProvider; applications do not receive or validate a Google token."
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/google-idp-brokering.md",
"heading": "Google IdP brokering",
"line_start": 1,
"line_end": 5,
"claim_ids": [],
"decision_ids": [],
"priority": 5.851474
},
{
"id": "La28755902d",
"title": "google claim to role — Google claim-to-role mapping",
"url": "repo:///docs/google-claim-to-role.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"# Google claim-to-role mapping\n\n`hd=example.test`인 upstream OIDC identity에는 Keycloak realm role\n`employee-role`을 부여한다. 매핑 키는 email이 아니라 Google subject이며,\nrole 조건에 쓰는 `hd` claim은 mock provider와 실제 Google provider에서 같은\n계약을 사용한다.\n\nRealm import는 `oidc-role-idp-mapper`를 선언한다. 실제 Google 설정 스크립트도\n같은 mapper를 upsert한다. 따라서 재실행해도 mapper가 중복되지 않는다.\n\n검증:\n\n```sh\n./scripts/verify-google-claim-to-role.sh\n```\n\n검증기는 mock Google 로그인, Authorization Code + PKCE 교환, 최종 Keycloak\naccess token의 `realm_access.roles`를 차례로 확인한다."
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/google-claim-to-role.md",
"heading": "Google claim-to-role mapping",
"line_start": 1,
"line_end": 18,
"claim_ids": [],
"decision_ids": [],
"priority": 4.750257
},
{
"id": "Le8474e5ddd",
"title": "https termination — HTTPS termination: nginx or Caddy",
"url": "repo:///docs/https-termination.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"# HTTPS termination: nginx or Caddy\n\n두 예제 모두 public `443`에서 TLS를 종료하고 private Docker network의\n`keycloak:8080`으로 전달한다. Keycloak 쪽 설정은\n`deploy/reverse-proxy/keycloak.env.example`의 hostname/proxy contract를\n같이 사용한다.\n\n- nginx: 인증서 배포·갱신을 운영자가 담당할 때 적합하다.\n- Caddy: ACME를 통한 인증서 수명주기를 proxy가 담당하게 할 때 간단하다.\n- 둘을 동시에 production entry point로 띄우지 않는다.\n- 인증서와 private key는 repository 또는 image에 포함하지 않는다.\n- HTTP challenge/redirect 및 방화벽의 80/443 허용은 배포 환경에서 별도로\n 결정한다.\n\n검증 스크립트는 임시 자체 서명 인증서를 만들고 두 vendor image에서 설정을\n각각 validate한 뒤 임시 파일을 제거한다.\n\n```sh\n./scripts/verify-https-termination-config.sh\n```"
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/https-termination.md",
"heading": "HTTPS termination: nginx or Caddy",
"line_start": 1,
"line_end": 20,
"claim_ids": [],
"decision_ids": [],
"priority": 2.684955
},
{
"id": "L0eb117abf5",
"title": "google redirect uri policy — Google redirect URI policy",
"url": "repo:///docs/google-redirect-uri-policy.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"# Google redirect URI policy\n\nGoogle에 등록하는 redirect URI는 애플리케이션 SPA callback이 아니라 Keycloak\nbroker endpoint다.\n\n```text\nhttps://auth.example.test/realms/keycloak-patterns/broker/google/endpoint\n```\n\n규칙:\n\n- production URI는 HTTPS와 고정된 public Keycloak origin을 사용한다.\n- wildcard, path prefix, 임시 tunnel hostname을 production OAuth client에\n 등록하지 않는다.\n- 개발·스테이징·운영은 Google OAuth client를 분리한다.\n- reverse proxy가 있더라도 Google이 보는 URI와 Keycloak이 생성하는 URI가\n byte-for-byte 같아야 한다.\n- `configure-google-idp.sh`가 출력하는 URI를 Google Console의 Authorized\n redirect URI와 대조한다.\n\n```sh\nPUBLIC_KEYCLOAK_URL=https://auth.example.test \\\n ./scripts/verify-google-redirect-uri-policy.sh\n```"
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/google-redirect-uri-policy.md",
"heading": "Google redirect URI policy",
"line_start": 1,
"line_end": 24,
"claim_ids": [],
"decision_ids": [],
"priority": 2.514945
},
{
"id": "L5d2c3b8016",
"title": "reverse proxy headers — Reverse proxy headers",
"url": "repo:///docs/reverse-proxy-headers.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"# Reverse proxy headers\n\nTLS를 reverse proxy에서 종료하면 Keycloak은 브라우저가 사용한 외부 origin을\n정확히 알아야 한다. 배포 예제는 다음 계약을 함께 적용한다.\n\n- nginx는 `Host`, `X-Forwarded-Host`, `X-Forwarded-Port`,\n `X-Forwarded-Proto`, `X-Forwarded-For`를 덮어쓴다.\n- Keycloak은 `KC_PROXY_HEADERS=xforwarded`로 그 헤더 형식을 명시한다.\n- `KC_HOSTNAME`은 외부 HTTPS URL로 고정하고 strict hostname 검증을 켠다.\n- Keycloak의 8080 포트는 public으로 publish하지 않고 proxy network에서만\n 접근시킨다. 신뢰되지 않은 클라이언트가 forwarded header를 직접 넣을 수\n 있으면 안 된다.\n\n`scripts/verify-reverse-proxy-headers.sh`는 양쪽 설정의 짝과 nginx 구문을\n검증한다."
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/reverse-proxy-headers.md",
"heading": "Reverse proxy headers",
"line_start": 1,
"line_end": 15,
"claim_ids": [],
"decision_ids": [],
"priority": 1.816984
},
{
"id": "L03b6abccb3",
"title": "google claim mapping — Google claim and identity mapping",
"url": "repo:///docs/google-claim-mapping.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"# Google claim and identity mapping\n\nThe broker uses the upstream OIDC `sub` as the stable federated identity key.\nEmail is a mutable profile attribute and is never the external identity key.\n\nThe default mapping policy is:\n\n| Upstream claim | Keycloak target |\n|---|---|\n| `sub` | stable username `${ALIAS}.${CLAIM.sub}` and federated identity ID |\n| `email` | email |\n| `given_name` | first name |\n| `family_name` | last name |\n| `picture` | custom `picture` attribute |\n| `hd` | custom `hd` attribute |\n\nThe Identity Provider uses `syncMode=IMPORT`: profile values are imported on\nfirst login and later local edits are not overwritten on every login. `FORCE`\nis an explicit alternative when upstream freshness is more important.\n\n`./scripts/verify-google-claim-mapping.sh` signs in through the controllable\nOIDC realm and verifies the resulting Keycloak user, custom attributes, stable\nsubject-derived username, and federated identity record."
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/google-claim-mapping.md",
"heading": "Google claim and identity mapping",
"line_start": 1,
"line_end": 23,
"claim_ids": [],
"decision_ids": [],
"priority": 1.503831
},
{
"id": "L0217277f31",
"title": "account linking sub vs email — Federated account key: `sub`, not email",
"url": "repo:///docs/account-linking-sub-vs-email.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"# Federated account key: `sub`, not email\n\n외부 IdP의 email은 표시·연락 속성이지 계정 식별자나 자동 연결 증명이 아니다.\nKeycloak의 federated identity는 provider alias와 provider user ID(`sub`)를\n로컬 사용자에 연결한다.\n\n정책:\n\n- 신규 identity의 email이 기존 로컬 계정과 충돌하면 기존 계정의 인증을 다시\n 요구하는 기본 First Broker Login flow를 사용한다.\n- `Automatically Set Existing User`를 production flow에 넣지 않는다.\n- upstream email 변경은 같은 `sub`의 계정 귀속을 바꾸지 않는다.\n- 마지막 로그인 수단을 unlink하는 UI에서는 먼저 다른 인증 수단을 등록하도록\n 안내한다.\n\n`verify-account-linking-sub-vs-email.sh`는 mock IdP 사용자의 email을 실제로\n변경하고 다시 로그인한다. 로컬 사용자 ID가 유지되고 federated `userId`가\nupstream `sub`와 같은지 확인한 후 원래 email을 복구한다."
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/account-linking-sub-vs-email.md",
"heading": "Federated account key: `sub`, not email",
"line_start": 1,
"line_end": 18,
"claim_ids": [],
"decision_ids": [],
"priority": 1.49767
},
{
"id": "L4a3b756b3d",
"title": "google idp brokering — Two verification profiles",
"url": "repo:///docs/google-idp-brokering.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"## Two verification profiles\n\nThe default local profile imports a second Keycloak realm named `mock-google`.\nIt acts as a controllable OIDC provider and allows tests to choose claims such\nas a duplicate email, `email_verified=false`, `hd`, and `picture`. This is the\nsafe way to reproduce an unsafe email auto-link without impersonating a real\nGoogle account.\n\nThe real-Google profile is configured explicitly:\n\n1. Create a Google OAuth **Web application**.\n2. Register the exact redirect URI printed by\n `./scripts/configure-google-idp.sh`.\n3. Put `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in ignored `.env`.\n4. Start the stack and run the configuration script.\n\nThe script writes `providerId=google`, `trustEmail=false`, minimal\n`openid profile email` scopes, and `syncMode=IMPORT` through the Keycloak Admin\nAPI. Credentials are never written to the realm export or repository.\n\nGoogle requires a public HTTPS redirect for non-local deployments. Local mock\nverification proves the Keycloak brokering boundary; a real Google login is a\nseparate credentialed acceptance profile."
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/google-idp-brokering.md",
"heading": "Two verification profiles",
"line_start": 6,
"line_end": 28,
"claim_ids": [],
"decision_ids": [],
"priority": 1.020519
},
{
"id": "Le9a41ffd86",
"title": "public domain tunneling — Public HTTPS domain for broker callbacks",
"url": "repo:///docs/public-domain-tunneling.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"# Public HTTPS domain for broker callbacks\n\nGoogle brokering을 반복 테스트할 때는 Cloudflare **named tunnel + 관리\n도메인**을 기본 profile로 사용한다. `trycloudflare.com` quick tunnel과\n임의 ngrok URL은 일회성 데모용이며 고정 callback으로 간주하지 않는다.\n\n설정 순서:\n\n1. `cloudflared tunnel login`\n2. `cloudflared tunnel create keycloak-patterns`\n3. 예제 config의 tunnel UUID와 credentials path를 실제 값으로 교체\n4. `cloudflared tunnel route dns keycloak-patterns auth.example.test`\n5. `cloudflared tunnel run keycloak-patterns`\n6. Keycloak `KC_HOSTNAME`과 Google redirect URI를 같은 public host로 설정\n\n컨테이너 안의 `127.0.0.1`은 cloudflared 컨테이너 자신이므로 origin에는\n`reverse-proxy:8080` 같은 Compose service DNS를 사용한다. 마지막 catch-all\ningress는 알 수 없는 hostname을 404로 끝낸다.\n\n실 tunnel 생성과 DNS 변경에는 사용자 소유 계정·도메인이 필요하므로 자동\n검증은 ingress 파일의 구조까지만 수행한다."
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/public-domain-tunneling.md",
"heading": "Public HTTPS domain for broker callbacks",
"line_start": 1,
"line_end": 21,
"claim_ids": [],
"decision_ids": [],
"priority": 0.729912
},
{
"id": "L55212df816",
"title": "first broker login security — First Broker Login security",
"url": "repo:///docs/first-broker-login-security.md",
"publisher": "local documentation corpus",
"accessed": "",
"facts": [
"# First Broker Login security\n\nKeycloak 26.7.0's built-in `first broker login` flow does **not** silently\nauto-link by email. It contains:\n\n- `Create User If Unique`\n- `Handle Existing Account`\n- `Confirm link existing account`\n- email verification or re-authentication ownership proof\n\n`Automatically set existing user` is an explicit, dangerous opt-in. The local\nacceptance harness copies the built-in flow, enables AutoLink, disables the\nownership-proof branch, and signs in through a controllable OIDC account whose\nemail collides with `regular-user`. It verifies that the external identity is\nattached without proof. The harness then assigns the original built-in flow,\nrepeats the login, observes the existing-account confirmation page, and verifies\nthat no federated identity was attached.\n\nRun after the stack is healthy:\n\n```bash\n./scripts/verify-first-broker-login.sh\n```\n\nThe vulnerable flow remains only as a disabled learning artifact. The\n`mock-google` provider is always returned to the secure built-in flow at the end\nof the verification."
],
"notes": "Internal retrieval excerpt. Preserve provenance in the sidecar evidence map; do not copy repository paths, source IDs, access dates, or process language into reader-facing prose.",
"source_type": "local-document",
"status": "",
"path": "docs/first-broker-login-security.md",
"heading": "First Broker Login security",
"line_start": 1,
"line_end": 27,
"claim_ids": [],
"decision_ids": [],
"priority": 0.088108
}
]
}