Files
llm-wiki/docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/lanes/lane-01-keycloak-sorted-01-10.md
T

54 KiB

Lane Inventory

Path Status Evidence Lines Extracted Facts
raw/branch-notes/experiment-nplus1-highlight-feed.md READ_FULL L1-L233 N+1 실험·측정 계약. Keycloak은 예정 형제 주제로만 언급되며 Keycloak 결정 없음.
raw/branch-notes/feature-authentication-authorization-contract.md READ_FULL L1-L341 application authz PEP, raw role→permission registry, Keycloak role claim 입력 경계.
raw/branch-notes/feature-boundary-validation-mapping-contract.md READ_FULL L1-L407 validation/mapping 경계 계약. Keycloak은 sample seed 설명에만 등장하며 Keycloak 결정 없음.
raw/branch-notes/feature-keycloak-account-linking-spa-ux.md READ_FULL L1-L228 SPA account-link UX, First Broker Login, link/unlink, CIAL, linked-account read path.
raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md READ_FULL L1-L279 sub federation key, local-account collision, re-auth, unlink lockout, Sync Mode.
raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md READ_FULL L1-L274 SPA Direct/BFF 선택, token holder, session, OAuth 2.1 조건.
raw/branch-notes/feature-keycloak-docker-compose-stack.md READ_FULL L1-L287 Keycloak/Postgres/app/nginx Compose topology, health, import, issuer/network.
raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md READ_FULL L1-L325 P1B Google brokering, P1A 불변식, account matching, child-owner 위임.
raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md READ_FULL L1-L316 P1A header-trust ForwardAuth, oauth2-proxy/nginx/Traefik, cookie session.
raw/branch-notes/feature-keycloak-federation-spa-zero-change.md READ_FULL L1-L193 P2A→P2B login-path zero-change, token claim baseline, account-link exclusion.

1. experiment-nplus1-highlight-feed.md

0-finding justification: Keycloak 일관성 관점에서 3개 항목을 확인했다.

  1. Keycloak ownership — 해당 없음: 예정 형제 주제로만 언급됨(:25).
  2. 인증 흐름 — 해당 없음: feed 측정은 HTTP 인증과 무관하다고 경계를 둠(:108).
  3. Decision Evidence Map — D-01~D-10은 N+1·CQRS 실험 결정이며 Keycloak 설정을 소유하지 않음(:64-77).

따라서 이 파일에 대한 Keycloak inconsistency finding은 0건이다. 문서 전체 품질 판정은 본 lane 범위가 아니다.

2. feature-authentication-authorization-contract.md

L1-F01: D3의 ROLE_* 입력과 실제 raw-role registry 계약이 다르다

  • Source file: raw/branch-notes/feature-authentication-authorization-contract.md
  • Source quote:

    AuthorizationPort 가 ROLE_* authorities → role→permission registry 확장 → 요구 permission 포함 판정. 매핑 source = app-side config 기본

  • Counterpart/current quote:

    ca-tmpl AuthenticatedUser.roles(adapter-web, Set<String>)는 raw role 문자열(admin/user — Keycloak 원본, prefix 없음)을 담는다.

  • Source lines: raw/branch-notes/feature-authentication-authorization-contract.md:124, raw/branch-notes/feature-authentication-authorization-contract.md:169
  • Severity: Medium
  • Classification: FACT — STALE_SUMMARY; 실패 영향은 INFERENCE.
  • Claim: D3는 registry 입력을 ROLE_* authority로 정의하지만 구현 명세는 prefix 없는 raw role을 canonical 입력으로 정의한다.
  • Real-world assumptions:
    1. 구현이 :169의 raw-role contract를 따른다.
      • Falsifies if: adapter가 registry 호출 전에 raw role을 ROLE_*로 변환한다.
      • Verify: AuthorizationAdapter 입력과 RolePermissionProperties key를 함께 확인한다.
    2. 운영자가 D3를 보고 config key를 작성한다.
      • Falsifies if: config schema가 raw key만 허용하고 startup에서 prefix를 거부한다.
      • Verify: application.yml binding test에 adminROLE_ADMIN 케이스를 추가한다.
  • Concrete failure mode: 문서대로 ROLE_ADMIN을 registry key로 넣고 runtime이 admin을 조회하면 fail-closed로 permission 0개가 되어 정당한 요청이 403으로 거부된다.
  • Falsification condition: ROLE_*는 Spring adapter 내부의 중간 표현일 뿐이며 D3가 raw-role 변환 경계를 명시하도록 이미 갱신돼 있다면 finding은 무효다.
  • Required action: D3를 “Keycloak raw role → adapter에서 Spring authority 파생, authz registry는 raw role consume”으로 고치고 security-baseline owner 결정을 pointer로 연결한다.
  • Why this action: as-built contract와 config vocabulary를 하나로 만들어 prefix drift를 없앤다.
  • Alternatives considered:
    1. Raw role canonical — 현재 코드와 맞고 문서만 수정한다.
    2. ROLE_* canonical — Spring layer에는 단순하지만 registry/config migration이 필요하다.
    3. Permission claim 직접 consume — IdP 중앙집중형에 맞지만 Keycloak 결합이 커진다.
    4. 명시적 principal DTO에서 raw role/authority 둘 다 보존 — 경계가 선명하지만 모델이 늘어난다.
  • Counterargument: D3의 ROLE_*가 단지 security-baseline 출력의 설명이고 실제 registry 입력은 :169에서 명확하다고 볼 수 있다. 사용자 확인 방법은 D3만 읽은 신규 운영자가 config key를 올바르게 작성할 수 있는지 문서 리뷰로 재현하는 것이다.
  • Required action verification: raw-role/authority 용어를 grep하고 D3·§3·config example이 같은 vocabulary를 쓰는지 확인한다.
  • Verification command and observed output:
$ grep -nFo -- 'AuthorizationPort 가 `ROLE_*` authorities → role→permission registry 확장 → 요구 permission 포함 판정. 매핑 source = app-side config 기본' 'raw/branch-notes/feature-authentication-authorization-contract.md'
124:AuthorizationPort 가 `ROLE_*` authorities → role→permission registry 확장 → 요구 permission 포함 판정. 매핑 source = app-side config 기본

$ grep -nFo -- 'ca-tmpl `AuthenticatedUser.roles`(`adapter-web`, `Set<String>`)는 **raw role 문자열**(`admin`/`user` — Keycloak 원본, prefix 없음)을 담는다.' 'raw/branch-notes/feature-authentication-authorization-contract.md'
169:ca-tmpl `AuthenticatedUser.roles`(`adapter-web`, `Set<String>`)는 **raw role 문자열**(`admin`/`user` — Keycloak 원본, prefix 없음)을 담는다.

Single-finding justification: authN/authZ ownership, permission model, failure mapping, method-security enforcement, Keycloak role input의 5개 항목을 검토했다. lane 내부에서 양립하지 않는 항목은 role vocabulary 1건이다. security-baseline의 실제 owner 결정은 unseen이므로 cross-lane 후보로 분리했다.

3. feature-boundary-validation-mapping-contract.md

0-finding justification: Keycloak 일관성 관점에서 3개 항목을 확인했다.

  1. In/Out scope와 decisions는 validation/mapping 책임만 다룸(:60-101).
  2. Decision Evidence Map D1~D15는 mapper·validation·ArchUnit 계약임(:258-278).
  3. Keycloak은 sample seed 설명에만 등장함(:350).

따라서 Keycloak 설계·흐름 inconsistency finding은 0건이다.

4. feature-keycloak-account-linking-spa-ux.md

  • Source file: raw/branch-notes/feature-keycloak-account-linking-spa-ux.md
  • Source quote:

    orphan 거부 메커니즘은 owner 브랜치 D3(역시 needs-confirmation)에서 추적

  • Counterpart/current quote:

    Account Console self-service unlink lockout 방지는 Keycloak 엔진이 서버에서 이미 강제

  • Source lines: raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:119, raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:152
  • Severity: Medium
  • Classification: FACT — STALE_SUMMARY.
  • Claim: owner D3가 engine-source-backed 결정으로 승격됐지만 SPA UX의 D4 요약은 여전히 needs-confirmation으로 남아 있다.
  • Real-world assumptions:
    1. 배포 버전에서도 owner가 인용한 unlink guard가 유지된다.
      • Falsifies if: 배포 release tag에서 guard가 제거·변경됐다.
      • Verify: 배포 tag의 LinkedAccountsResource와 dev unlink 응답을 확인한다.
  • Concrete failure mode: SPA UX 설계자가 server guard를 모른 채 중복 lockout API를 만들거나, 반대로 상태를 미확정으로 보고 UX 계약을 계속 보류한다.
  • Falsification condition: 배포 버전 검증이 실패해 owner D3가 다시 needs-confirmation으로 내려가면 현재 요약은 stale하지 않다.
  • Required action: D4·§Unlink·Claims의 요약을 [[...sub-vs-email]] D3 — server guard confirmed; release-tag verification remains 한 줄로 갱신한다.
  • Why this action: evidence strength와 deployment-specific risk를 동시에 보존한다.
  • Alternatives considered:
    1. Pointer + current summary — 읽기 편하고 drift 전파가 필요하다.
    2. Pointer only — drift가 적지만 UX 문맥이 약해진다.
    3. Version-pinned summary — 정확하지만 Keycloak upgrade마다 갱신 비용이 든다.
  • Counterargument: owner 근거가 main branch이므로 배포판에는 여전히 불확실성이 있다. 다만 이는 UNSUPPORTED가 아니라 “source-backed, release-tag 확인 필요” 상태다.
  • Verification command and observed output:
$ grep -nFo -- 'orphan 거부 메커니즘은 owner 브랜치 D3(역시 `needs-confirmation`)에서 추적' 'raw/branch-notes/feature-keycloak-account-linking-spa-ux.md'
119:orphan 거부 메커니즘은 owner 브랜치 D3(역시 `needs-confirmation`)에서 추적

$ grep -nFo -- 'Account Console self-service unlink lockout 방지는 **Keycloak 엔진이 서버에서 이미 강제**' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
152:Account Console self-service unlink lockout 방지는 **Keycloak 엔진이 서버에서 이미 강제**

L1-F03: delegated email_verified 정책이 active owned decision으로도 남아 있다

  • Source file: raw/branch-notes/feature-keycloak-account-linking-spa-ux.md
  • Source quote:

    email_verified=true만 link 허용

  • Counterpart/current quote:

    email_verified 는 Keycloak linking gate 가 아니라 Trust Email(계정 생성 시 verified 표시) 설정

  • Source lines: raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:102, raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:117
  • Severity: Medium
  • Classification: FACT — RESTATED_FOREIGN_DECISION + ambiguous mechanism.
  • Claim: active Decisions는 이 branch가 link gate를 소유하는 것처럼 단정하지만, DEM은 owner 위임과 Trust Email/custom-step 구분을 기록한다.
  • Real-world assumptions:
    1. 구현자가 :102를 Keycloak 기본 Trust Email 설정으로 해석한다.
      • Falsifies if: owner D2가 custom First Broker Login step을 구현하고 참조자가 이를 명시한다.
      • Verify: unseen idp-mappers D2와 realm export의 authenticator 구성을 확인한다.
  • Concrete failure mode: Trust Email=true만으로 linking을 제한했다고 오인해 Confirm Link/re-auth 통제가 빠지고, threat model과 실제 flow가 달라진다.
  • Falsification condition: owner D2가 email_verified custom gate를 실제 소유·구현하고 :102가 그 pointer 요약으로만 읽히도록 고쳐졌다면 finding은 해소된다.
  • Required action: :102의 owned decision을 제거하고 owner D2 pointer로 치환하며, Trust Email과 custom linking gate를 별개 용어로 적는다.
  • Why this action: Reference-Only를 지키면서 설정 lever 혼동을 막는다.
  • Alternatives considered:
    1. Trust Email만 사용 — 계정 생성 verified 표시에 적합하지만 linking proof는 아니다.
    2. Custom email_verified gate — 정책은 강하지만 SPI/flow 구현이 필요할 수 있다.
    3. Confirm Link + secret proof — account ownership을 검증하지만 UX 마찰이 있다.
    4. Collision linking 비활성화 — 단순하지만 기존 local account 연결을 제공하지 않는다.
  • Counterargument: :102가 native setting이 아니라 상위 정책을 말한 것이라면 행동 자체는 유지할 수 있다. 그래도 owner와 mechanism을 함께 명시해야 오독을 막을 수 있다.
  • Verification command and observed output:
$ grep -nFo -- '**`email_verified=true`만 link 허용**' 'raw/branch-notes/feature-keycloak-account-linking-spa-ux.md'
102:**`email_verified=true`만 link 허용**

$ grep -nFo -- '`email_verified` 는 Keycloak *linking gate* 가 아니라 Trust Email(계정 생성 시 verified 표시) 설정' 'raw/branch-notes/feature-keycloak-account-linking-spa-ux.md'
117:`email_verified` 는 Keycloak *linking gate* 가 아니라 Trust Email(계정 생성 시 verified 표시) 설정

L1-F04: SPA UX가 password 확인을 고정 흐름으로 말하지만 lane owner 후보는 email verification 기본을 기록한다

  • Source file: raw/branch-notes/feature-keycloak-account-linking-spa-ux.md
  • Source quote:

    또는 로그인 화면에서 Google로 처음 로그인했을 때 같은 email의 기존 계정이 있으면 'Confirm Link Existing Account' authenticator가 비밀번호 확인을 요구합니다.

  • Counterpart/current quote:

    SMTP 설정 realm 은 "Verify Existing Account By Email" 이 기본 ALTERNATIVE 이므로 admin 이 명시 DISABLE 해야 password 재인증 실행

  • Source lines: raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:34, raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:151
  • Severity: High
  • Classification: FACT — CONTRADICTION; authoritative flow owner는 unseen이므로 NEEDS_CROSS_LANE_AUDIT.
  • Claim: 동일 email collision UX의 기본 verification method가 password와 email 두 가지로 서술된다.
  • Real-world assumptions:
    1. realm에 SMTP가 설정돼 있고 built-in first-broker-login flow를 사용한다.
      • Falsifies if: Email authenticator가 disabled이거나 SMTP가 없다.
      • Verify: First Broker Login execution config와 realm SMTP 설정을 확인한다.
    2. SPA UX 문장이 사용자 안내·테스트 기대값으로 사용된다.
      • Falsifies if: 문서가 security-first profile 한정이라고 표시돼 있다.
      • Verify: UX copy와 E2E expected step을 비교한다.
  • Concrete failure mode: 문서는 password 화면을 기대하지만 실제 realm은 email verification을 보여 테스트·사용자 안내가 어긋나거나, security-first 의도와 달리 secret proof가 생략된다.
  • Falsification condition: first-broker owner D2가 Email authenticator disable을 고정하고 realm export가 이를 보장한다면 SPA 문장은 성립한다.
  • Required action: first-broker owner에서 email-defaultpassword-step-up profile 중 하나를 선택하고, SPA UX는 그 D-row를 pointer+1줄로만 참조한다.
  • Why this action: UX, threat model, realm execution config를 같은 선택 조건에 묶는다.
  • Alternatives considered:
    1. Email verification 기본 — SMTP 운영이 가능하고 마찰을 낮출 때.
    2. Password re-auth 강제 — 기존 local secret 소유 증명이 필요할 때.
    3. 조건부 step-up — 위험도에 따라 email/password를 나눌 때.
    4. 기존-account linking 미제공 — duplicate account 비용을 감수할 때.
  • Counterargument: 이 문장이 이미 security-first profile을 전제한다고 해석할 수 있다. 그러나 선택 조건이 본문에 없고 default flow 설명처럼 쓰였다는 점은 남는다.
  • Verification command and observed output:
$ grep -nFo -- "또는 로그인 화면에서 Google로 처음 로그인했을 때 같은 email의 기존 계정이 있으면 'Confirm Link Existing Account' authenticator가 비밀번호 확인을 요구합니다." 'raw/branch-notes/feature-keycloak-account-linking-spa-ux.md'
34:또는 로그인 화면에서 Google로 처음 로그인했을 때 같은 email의 기존 계정이 있으면 'Confirm Link Existing Account' authenticator가 비밀번호 확인을 요구합니다.

$ grep -nFo -- 'SMTP 설정 realm 은 "Verify Existing Account By Email" 이 기본 `ALTERNATIVE` 이므로 admin 이 명시 DISABLE 해야 password 재인증 실행' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
151:SMTP 설정 realm 은 "Verify Existing Account By Email" 이 기본 `ALTERNATIVE` 이므로 admin 이 명시 DISABLE 해야 password 재인증 실행

5. feature-keycloak-account-linking-sub-vs-email.md

L1-F05: active Sync Mode decision의 takeover 근거가 current DEM과 반대다

  • Source file: raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md
  • Source quote:

    Sync Mode = IMPORT (first login만). Google 측 email 변경이 Keycloak으로 자동 전파되지 않음 → Scenario B 회피.

  • Counterpart/current quote:

    본 branch(takeover)와 직교 — Sync Mode 는 이미 링크된 계정의 attribute(email/name) 최신성(FORCE) vs 안정성(IMPORT) trade-off 이며 link key(sub)를 안 바꾸므로 takeover 무관.

  • Source lines: raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:137, raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:154
  • Severity: Medium
  • Classification: FACT — STALE_SUMMARY + RESTATED_FOREIGN_DECISION.
  • Claim: Decisions는 IMPORT가 Scenario B를 방어한다고 말하지만 current D5는 Sync Mode와 takeover가 직교하며 값 owner도 sibling이라고 정정한다.
  • Real-world assumptions:
    1. 운영자가 takeover 방어를 이유로 IMPORT를 선택한다.
      • Falsifies if: IMPORT는 attribute ownership 정책으로 별도 채택된다.
      • Verify: realm export와 ADR의 선택 근거를 확인한다.
  • Concrete failure mode: 보안상 필요하지 않은 IMPORT가 고정돼 Google-authoritative profile freshness 요구를 막거나, FORCE를 선택하면 takeover가 생긴다는 잘못된 경보를 만든다.
  • Falsification condition: IMPORT가 로컬 attribute 편집 보존이라는 별도 요구로 선택되고 active Decisions의 이유가 갱신되면 finding은 사라진다.
  • Required action: :137을 historical decision으로 이동하고 active D5는 google-claim owner D3 pointer와 freshness/stability 조건만 남긴다.
  • Why this action: federation key 보안과 attribute synchronization을 독립 축으로 유지한다.
  • Alternatives considered:
    1. IMPORT — 로컬 편집을 보존할 때.
    2. FORCE — Google을 attribute authority로 둘 때.
    3. INHERIT/LEGACY — realm default를 따르지만 upgrade drift를 감수한다.
    4. Owner-only delegation — 이 노트는 takeover 영향만 “직교”로 기록한다.
  • Counterargument: email 충돌의 운영 영향을 줄이기 위해 IMPORT가 여전히 유효할 수 있다. 다만 그 이유는 takeover 방지가 아니라 attribute stability다.
  • Verification command and observed output:
$ grep -nFo -- 'Sync Mode = IMPORT (first login만). Google 측 email 변경이 Keycloak으로 자동 전파되지 않음 → Scenario B 회피.' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
137:Sync Mode = IMPORT (first login만). Google 측 email 변경이 Keycloak으로 자동 전파되지 않음 → Scenario B 회피.

$ grep -nFo -- '**본 branch(takeover)와 직교** — Sync Mode 는 이미 링크된 계정의 attribute(email/name) 최신성(FORCE) vs 안정성(IMPORT) trade-off 이며 link key(sub)를 안 바꾸므로 takeover 무관.' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
154:**본 branch(takeover)와 직교** — Sync Mode 는 이미 링크된 계정의 attribute(email/name) 최신성(FORCE) vs 안정성(IMPORT) trade-off 이며 link key(sub)를 안 바꾸므로 takeover 무관.

L1-F06: persistent federation key와 local-account collision locator가 sub-only 하나로 합쳐져 있다

  • Source file: raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md
  • Source quote:

    federated identity primary key = Google sub claim 만 (email 은 link key 아님) — 본 branch core owned

  • Counterpart/current quote:

    OOTB 는 email/username 매칭 → sub-only 원하면 커스텀 authenticator (별도 branch)

  • Source lines: raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:150, raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:172
  • Severity: High
  • Classification: FACT — ambiguous semantics / incompatible scope assumption; 실패 영향은 INFERENCE.
  • Claim: 이미 연결된 federated identity의 안정 key와, 아직 연결되지 않은 local account 후보를 찾는 locator가 구분되지 않아 D1을 구현하려면 out-of-scope custom authenticator가 필요하다는 결론에 이른다.
  • Real-world assumptions:
    1. 기존 local account에는 아직 Google sub mapping이 없다.
      • Falsifies if: 외부 directory에 local-user↔Google-sub 사전 매핑이 존재한다.
      • Verify: first-login 전 FEDERATED_IDENTITY와 별도 mapping source를 확인한다.
    2. OOTB flow가 email/username으로 후보를 찾고 별도 verification을 수행한다.
      • Falsifies if: 배포판에 sub-based collision authenticator가 있다.
      • Verify: owner flow와 authenticator source를 확인한다.
  • Concrete failure mode: “sub-only collision detection”을 그대로 구현하면 기존 local account를 찾지 못해 duplicate user가 생기거나, custom authenticator가 제외돼 D1이 실행 불가능한 정책으로 남는다.
  • Falsification condition: D1의 “link key”가 오직 persistent federated identity lookup만 의미하고 email candidate discovery+re-auth를 허용한다고 명시하면 모순은 용어 문제로 축소된다.
  • Required action: 결정을 두 개로 분리한다: (IdP alias, external sub)는 persistent federation key, email/username은 existing-account 후보 locator일 수 있으나 link 승인 proof는 아니다.
  • Why this action: stable identity와 initial discovery의 서로 다른 위협 모델을 분리한다.
  • Alternatives considered:
    1. Email candidate + re-auth — OOTB 활용, email은 proof가 아님.
    2. 기존 계정 로그인 후 CIAL — 안정적이나 사용자가 두 단계 수행한다.
    3. 사전 directory mapping — 기업 환경에 맞지만 동기화 운영이 필요하다.
    4. 항상 신규 계정 생성 — 단순하지만 account fragmentation을 감수한다.
  • Counterargument: “primary key”가 DB 저장 key만 뜻했다면 D1 자체는 맞다. 이 경우 custom authenticator 요구를 D1에서 분리하고 용어만 바로잡으면 된다.
  • Verification command and observed output:
$ grep -nFo -- 'federated identity primary key = Google `sub` claim 만 (email 은 link key 아님) — **본 branch core owned**' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
150:federated identity primary key = Google `sub` claim 만 (email 은 link key 아님) — **본 branch core owned**

$ grep -nFo -- 'OOTB 는 email/username 매칭 → sub-only 원하면 커스텀 authenticator (별도 branch)' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
172:OOTB 는 email/username 매칭 → sub-only 원하면 커스텀 authenticator (별도 branch)

6. feature-keycloak-bff-vs-spa-direct.md

L1-F07: 요약 매트릭스가 OAuth 2.1의 조건부 BFF 권고를 일반 권고처럼 표시한다

  • Source file: raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md
  • Source quote:

    | OAuth 2.1 draft 권고 | (허용) | (browser-based app) |

  • Counterpart/current quote:

    public client + PKCE 만이면 SPA Direct 도 표준 허용 — "browser-based app 전반 의무" 아님

  • Source lines: raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md:130, raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md:165
  • Severity: Medium
  • Classification: FACT — STALE_SUMMARY.
  • Claim: 상단 matrix는 browser-based app이면 BFF가 표준 우위인 것처럼 보이지만 D5는 client credentials 사용 시에만 해당한다고 제한한다.
  • Real-world assumptions:
    1. 독자가 matrix를 architecture choice의 요약 SSOT로 사용한다.
      • Falsifies if: D5 조건을 반드시 함께 읽도록 표가 연결돼 있다.
      • Verify: matrix만 떼어 ADR 입력으로 사용해도 조건이 보이는지 확인한다.
  • Concrete failure mode: public SPA+PKCE가 요구에 맞는데도 “표준 권고”를 이유로 stateful BFF를 선택해 session store·CSRF 통제가 불필요하게 추가된다.
  • Falsification condition: 조직 보안 정책이 표준과 별개로 BFF를 요구하면 선택은 유지할 수 있지만 표준 근거의 일반화는 여전히 수정 대상이다.
  • Required action: matrix cell을 “client credentials가 필요한 browser app에서 BFF”로 좁히고 D5 pointer를 붙인다.
  • Why this action: 표준 조건과 프로젝트 risk preference를 분리한다.
  • Alternatives considered:
    1. Public SPA + PKCE — stateless와 multi-client가 중요할 때.
    2. Confidential BFF — browser token exposure를 줄이고 server session을 운영할 때.
    3. Edge ForwardAuth — 여러 backend가 공통 ingress 인증을 쓸 때.
    4. Token-mediating backend — session과 API fan-out을 절충할 때.
  • Counterargument: 금융·의료 등 내부 정책이 BFF를 요구할 수 있다. 그 경우 근거는 내부 risk decision으로 적어야 한다.
  • Verification command and observed output:
$ grep -nFo -- '| OAuth 2.1 draft 권고 | (허용) | ✅ (browser-based app) |' 'raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md'
130:| OAuth 2.1 draft 권고 | (허용) | ✅ (browser-based app) |

$ grep -nFo -- 'public client + PKCE 만이면 SPA Direct 도 표준 허용 — "browser-based app 전반 의무" 아님' 'raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md'
165:public client + PKCE 만이면 SPA Direct 도 표준 허용 — "browser-based app 전반 의무" 아님

Single-finding justification: token holder, session state, Spring client type, mobile split, OAuth 2.1 조건의 5개 항목을 검토했다. lane 내부의 명시적 mismatch는 OAuth 2.1 matrix 요약 1건이다. P2A/token-storage/refresh/audience owner 내용은 unseen이라 cross-lane으로 분리했다.

7. feature-keycloak-docker-compose-stack.md

L1-F08: bridge-network 기본값과 app의 localhost issuer URL은 함께 동작하지 않는다

  • Source file: raw/branch-notes/feature-keycloak-docker-compose-stack.md
  • Source quote:

    Spring Boot Resource Server, issuer-uri=http://localhost:8080/realms/<realm>

  • Counterpart/current quote:

    target env = 단일 EC2 Linux 를 기본 가정하므로 기본값은 extra_hosts: [host.docker.internal:host-gateway] (backend 가 host.docker.internal:8080 로 JWKS/issuer 도달).

  • Source lines: raw/branch-notes/feature-keycloak-docker-compose-stack.md:138, raw/branch-notes/feature-keycloak-docker-compose-stack.md:207
  • Severity: High
  • Classification: FACT — CONTRADICTION.
  • Claim: app container는 host.docker.internal로 host-published Keycloak에 접근한다고 선택했지만 Resource Server URL은 app container 자신을 가리키는 localhost로 남아 있다.
  • Real-world assumptions:
    1. app과 Keycloak이 별도 bridge containers로 실행된다.
      • Falsifies if: app이 host process이거나 network_mode: host를 쓴다.
      • Verify: rendered Compose의 network mode를 확인한다.
    2. Resource Server가 startup에 issuer-uri discovery를 수행한다.
      • Falsifies if: reachable jwk-set-uri를 별도로 두고 issuer validation만 localhost로 한다.
      • Verify: Spring security property와 startup log의 discovery URL을 확인한다.
  • Concrete failure mode: app이 자기 container의 localhost:8080으로 discovery/JWKS를 호출해 connection refused로 기동 실패하거나 첫 JWT 검증에서 실패한다.
  • Falsification condition: 실제 default가 network_mode: host이고 port-table 무효화를 수용한다면 localhost는 도달 가능하다.
  • Required action: public issuer와 internal retrieval URL을 분리하거나, browser와 containers가 함께 resolve하는 hostname을 채택한다.
  • Why this action: token iss 일치와 network reachability를 동시에 만족시킨다.
  • Alternatives considered:
    1. Host network + localhost issuer — Linux 한정, ports: 의미가 사라진다.
    2. Public issuer-uri + internal jwk-set-uri — issuer는 유지하고 JWKS만 reachable URL로 가져온다.
    3. 공통 DNS hostname — browser/app/Keycloak이 같은 host를 resolve하도록 한다.
    4. app을 host에서 실행 — 학습은 단순하지만 4-service Compose 목표가 바뀐다.
  • Counterargument: 이 노트는 iss mismatch 실험을 의도하므로 실패 구성이 교육용일 수 있다. 그러나 P3A “정상 기동 stack”과 “함정 시연” profile을 분리해야 한다.
  • Verification command and observed output:
$ grep -nFo -- 'Spring Boot Resource Server, `issuer-uri=http://localhost:8080/realms/<realm>`' 'raw/branch-notes/feature-keycloak-docker-compose-stack.md'
138:Spring Boot Resource Server, `issuer-uri=http://localhost:8080/realms/<realm>`

$ grep -nFo -- '**target env = 단일 EC2 Linux 를 기본 가정하므로 기본값은 `extra_hosts: [host.docker.internal:host-gateway]`** (backend 가 `host.docker.internal:8080` 로 JWKS/issuer 도달).' 'raw/branch-notes/feature-keycloak-docker-compose-stack.md'
207:**target env = 단일 EC2 Linux 를 기본 가정하므로 기본값은 `extra_hosts: [host.docker.internal:host-gateway]`** (backend 가 `host.docker.internal:8080` 로 JWKS/issuer 도달).

Single-finding justification: service topology, DB env, health port, realm import, secret mapping, issuer/network의 6개 항목을 검토했다. 다른 항목은 선택 조건·risk가 함께 적혀 있으며, 함께 선택된 default끼리 양립하지 않는 것은 issuer reachability 1건이다.

8. feature-keycloak-edge-forwardauth-google-federation.md

L1-F09: parent D5와 child D1이 같은 sub mapping 결정을 각각 소유한다

  • Source file: raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md
  • Source quote:

    사용자 매핑 식별자 = Google sub (영구 식별자), email 은 primary key 금지

  • Ownership quote:

    이 브랜치의 D1~D5 가 이들의 설정 SSOT.

  • Counterpart quote:

    federated identity primary key = Google sub claim 만 (email 은 link key 아님) — 본 branch core owned

  • Source lines: raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:195, raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:253, raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:150
  • Severity: High
  • Classification: FACT — DUAL_OWNERSHIP + RESTATED_FOREIGN_DECISION.
  • Claim: 동일 concern인 Google sub federation key를 parent D5와 child D1이 모두 owner로 선언한다.
  • Real-world assumptions:
    1. 두 D-row가 같은 persistent mapping key를 뜻한다.
      • Falsifies if: parent는 pattern requirement, child는 별도 implementation mechanism을 소유한다고 concern을 분리한다.
      • Verify: concern 이름과 change authority를 owner map에서 비교한다.
  • Concrete failure mode: child가 candidate-discovery와 sub semantics를 정정해도 parent D5가 그대로 남아 참조자가 서로 다른 결론을 가져간다.
  • Falsification condition: parent D5가 “requirement only”라는 별도 concern ID를 갖고 child D1만 concrete key policy owner임을 명시하면 dual ownership이 해소된다.
  • Required action: 한 문서만 D-row owner로 남긴다. 권장 구조는 parent가 P1B topology만 소유하고 D5를 child D1 pointer+1줄 요약으로 바꾸는 것이다.
  • Why this action: child의 위협 모델·evidence 변화가 한 owner에서 전파되게 한다.
  • Alternatives considered:
    1. Child owner, parent pointer — 현재 문서 분해와 가장 잘 맞는다.
    2. Parent owner, child 분석-only — hub가 무거워지지만 owner는 하나다.
    3. Child를 parent로 병합 — drift는 줄지만 문서 깊이가 커진다.
    4. Requirement/mechanism concern 분리 — 두 owner를 유지하되 동일 결정이 아니도록 명명한다.
  • Counterargument: parent D5가 requirement이고 child D1이 상세라고 볼 수 있다. 그러나 현재 두 문구는 같은 key와 email 금지를 결정하며 경계가 명시되지 않았다.
  • Verification command and observed output:
$ grep -nFo -- '사용자 매핑 식별자 = Google `sub` (영구 식별자), email 은 primary key 금지' 'raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md'
195:사용자 매핑 식별자 = Google `sub` (영구 식별자), email 은 primary key 금지

$ grep -nFo -- '이 브랜치의 D1~D5 가 이들의 설정 SSOT.' 'raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md'
253:이 브랜치의 D1~D5 가 이들의 설정 SSOT.

$ grep -nFo -- 'federated identity primary key = Google `sub` claim 만 (email 은 link key 아님) — **본 branch core owned**' 'raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md'
150:federated identity primary key = Google `sub` claim 만 (email 은 link key 아님) — **본 branch core owned**

L1-F10: P1B가 “P1A와 동일”이라면서 backend trust를 JWT signature로 바꾼다

  • Source file: raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md
  • Source quote:

    | Backend의 token 검증 | Keycloak signature | 동일 (Google token은 backend까지 안 옴) |

  • Counterpart quote:

    백엔드는 JWT 검증을 하지 않고 헤더만 신뢰

  • Source lines: raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:145, raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:112
  • Severity: High
  • Classification: FACT — CONTRADICTION.
  • Claim: P1A는 header-only trust를 정의하지만 P1B 비교표는 P1A와 P1B 모두 backend가 Keycloak signature를 검증한다고 요약한다.
  • Real-world assumptions:
    1. P1B가 P1A에 Google leg만 추가하는 변형이다.
      • Falsifies if: P1B가 defense-in-depth JWT validation을 추가하는 별도 variant다.
      • Verify: backend dependency/config diff를 확인한다.
    2. backend가 한 trust contract를 따라 구현된다.
      • Falsifies if: route별로 header-only와 bearer validation을 명시적으로 나눈다.
      • Verify: ingress header/token forwarding과 SecurityFilterChain을 대조한다.
  • Concrete failure mode: 구현팀 일부는 ingress header만 신뢰하고 다른 일부는 bearer token을 요구해, 같은 보호 route에서 401 또는 spoofing 방어 수준 차이가 발생한다.
  • Falsification condition: P1B가 새 variant임을 명시하고 “P1A와 동일” 문구를 제거하면 contradiction이 아니다.
  • Required action: P1 contract를 header-only, JWT-only, 또는 header+JWT 중 하나로 고정하고 P1B는 그 contract를 그대로 참조한다.
  • Why this action: Google federation 추가와 backend trust 변경을 독립 decision으로 만든다.
  • Alternatives considered:
    1. Header-only — backend 코드가 적지만 ingress isolation이 load-bearing이다.
    2. JWT-only — backend가 self-contained validation을 수행한다.
    3. Header + JWT AND 검증 — 방어가 늘지만 운영·코드 비용도 늘어난다.
    4. Route별 variant — 유연하지만 routing contract가 필요하다.
  • Counterargument: oauth2-proxy가 access token을 선택적으로 넘기므로 backend마다 다르게 쓸 수 있다. 그렇다면 “동일 패턴”이 아니라 두 variant를 명명해야 한다.
  • Verification command and observed output:
$ grep -nFo -- '| Backend의 token 검증 | Keycloak signature | 동일 (Google token은 backend까지 안 옴) |' 'raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md'
145:| Backend의 token 검증 | Keycloak signature | 동일 (Google token은 backend까지 안 옴) |

$ grep -nFo -- '**백엔드는 JWT 검증을 하지 않고 헤더만 신뢰**' 'raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md'
112:**백엔드는 JWT 검증을 하지 않고 헤더만 신뢰**

9. feature-keycloak-edge-forwardauth-no-google.md

L1-F11: oauth2-proxy와 Traefik ForwardAuth를 대안으로 놓지만 구현안에서는 함께 사용한다

  • Source file: raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md
  • Source quote:

    oauth2-proxy vs Traefik ForwardAuth plugin 선택 기준 정리.

  • Counterpart quote:

    oauth2-proxy /oauth2/auth 로 위임, 2XX=allow + 원본 요청 진행

  • Source lines: raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:167, raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:227
  • Severity: High
  • Classification: FACT — CONTRADICTION / incompatible alternatives.
  • Claim: D1은 oauth2-proxy와 Traefik ForwardAuth를 교체 가능한 단일 축으로 취급하지만 Traefik middleware는 실제로 oauth2-proxy auth endpoint를 호출한다.
  • Real-world assumptions:
    1. “Traefik ForwardAuth”는 middleware 자체를 뜻하며 OIDC session provider가 아니다.
      • Falsifies if: 문서가 별도 all-in-one traefik-forward-auth 제품을 뜻한다.
      • Verify: image/name/config와 forwardAuth.address target을 확인한다.
  • Concrete failure mode: “Traefik 선택”만으로 OIDC callback/session 기능이 생긴다고 보고 oauth2-proxy 같은 auth service를 배치하지 않아 로그인 흐름이 성립하지 않는다.
  • Falsification condition: 정확한 auth provider 제품을 명명하고 Traefik middleware와의 역할을 분리하면 finding은 해소된다.
  • Required action: 선택 축을 (Ingress: nginx|Traefik)(Auth service: oauth2-proxy|다른 OIDC agent)의 2차원으로 다시 작성한다.
  • Why this action: middleware와 identity-aware auth service의 책임을 분리한다.
  • Alternatives considered:
    1. Nginx + oauth2-proxyauth_request 방식.
    2. Traefik + oauth2-proxyforwardAuth.address 방식.
    3. Traefik + 별도 ForwardAuth service — 대체 provider를 명시한다.
    4. oauth2-proxy reverse-proxy mode — 별도 ingress subrequest 없이 upstream을 직접 proxy한다.
  • Counterargument: D1이 “Ingress-Nginx stack과 Traefik stack”을 줄여 쓴 표현일 수 있다. 그러나 현재 제목과 표는 component 대 component 비교로 읽힌다.
  • Verification command and observed output:
$ grep -nFo -- 'oauth2-proxy vs Traefik ForwardAuth plugin 선택 기준 정리.' 'raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md'
167:oauth2-proxy vs Traefik ForwardAuth plugin 선택 기준 정리.

$ grep -nFo -- 'oauth2-proxy `/oauth2/auth` 로 위임, 2XX=allow + 원본 요청 진행' 'raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md'
227:oauth2-proxy `/oauth2/auth` 로 위임, 2XX=allow + 원본 요청 진행

L1-F12: “브라우저에 access token 없음”과 “access token을 cookie에 저장”이 같은 threat model에 섞여 있다

  • Source file: raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md
  • Source quote:

    토큰 클라이언트 노출 없음: access_token을 브라우저에 두지 않음 → XSS로 토큰이 탈취될 surface 제거. BFF-like.

  • Counterpart quote:

    세션 쿠키 크기: access_token까지 쿠키에 저장하면 nginx의 기본 4kb 헤더 한도를 넘어 분할 쿠키 처리 로직 필요.

  • Source lines: raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:120, raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:127
  • Severity: Medium
  • Classification: FACT — CONTRADICTION / incompatible token-storage assumptions.
  • Claim: server-side session과 encrypted cookie session을 구분하지 않아 browser token possession과 JS raw-token readability가 같은 의미로 쓰인다.
  • Real-world assumptions:
    1. oauth2-proxy가 cookie-backed session에 token material을 넣을 수 있다.
      • Falsifies if: chosen config가 Redis/server-side store만 사용하고 cookie에는 opaque ID만 둔다.
      • Verify: oauth2-proxy session-store type과 cookie payload 크기를 확인한다.
  • Concrete failure mode: 보안 설계가 server-side BFF 수준의 token custody를 가정하지만 실제로는 browser-held encrypted cookie가 bearer-equivalent session이 되어 replay·size·rotation 위험이 남는다.
  • Falsification condition: cookie가 opaque session identifier뿐이고 token은 server store에만 있으면 :120의 보관 경계가 성립한다.
  • Required action: session mode를 결정하고 “raw token JS 접근”, “encrypted cookie 보유”, “server-side token 보관”을 별도 행으로 기술한다.
  • Why this action: XSS exfiltration, cookie replay, header-size 문제를 각각 맞는 통제로 연결한다.
  • Alternatives considered:
    1. Redis/server-side session — token custody가 명확하지만 state store가 필요하다.
    2. Opaque cookie ID + server memory — 단일 인스턴스 학습에 맞지만 scale-out이 약하다.
    3. Encrypted cookie에 token 포함 — 운영은 단순하지만 size/replay 경계를 문서화해야 한다.
    4. Access token upstream 전달 — backend JWT validation contract가 추가된다.
  • Counterargument: httpOnly encrypted cookie는 JS가 raw access token을 읽지 못하므로 “직접 노출 없음”이라는 보안 이점은 남는다. 다만 “브라우저에 두지 않음”이라는 물리적 보관 문구는 좁혀야 한다.
  • Verification command and observed output:
$ grep -nFo -- '**토큰 클라이언트 노출 없음**: `access_token`을 브라우저에 두지 않음 → XSS로 토큰이 탈취될 surface 제거. BFF-like.' 'raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md'
120:**토큰 클라이언트 노출 없음**: `access_token`을 브라우저에 두지 않음 → XSS로 토큰이 탈취될 surface 제거. BFF-like.

$ grep -nFo -- '**세션 쿠키 크기**: access_token까지 쿠키에 저장하면 nginx의 기본 4kb 헤더 한도를 넘어 분할 쿠키 처리 로직 필요.' 'raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md'
127:**세션 쿠키 크기**: access_token까지 쿠키에 저장하면 nginx의 기본 4kb 헤더 한도를 넘어 분할 쿠키 처리 로직 필요.

10. feature-keycloak-federation-spa-zero-change.md

L1-F13: zero-change는 hosted-login 경로에만 성립하지만 제목·목표는 P2B 전체처럼 읽힌다

  • Source file: raw/branch-notes/feature-keycloak-federation-spa-zero-change.md
  • Source quote:

    P2A 코드 그대로 두고 Keycloak에 Google IdP만 추가해서 SPA가 변경 없이 Google 로그인 가능함을 실증한다.

  • Counterpart quote:

    SPA 가 link 를 트리거하는 공식 메커니즘 = Client Initiated Account Linking (서명된 redirect URL fabrication)

  • Scope quote:

    Account Linking 정책 — raw/branch-notes/feature-keycloak-account-linking-spa-ux

  • Source lines: raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:31, raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:105, raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:53
  • Severity: Medium
  • Classification: FACT — ambiguous scope / incompatible defaults.
  • Claim: hosted Keycloak login button만 쓰면 login entry는 zero-change지만, in-SPA account linking을 포함하면 URL fabrication·hashing·상태 표시 코드가 필요하다.
  • Real-world assumptions:
    1. 상위 P2B 독자가 “Google federation 기능 전체”에 zero-change를 일반화한다.
      • Falsifies if: 상위 문서도 hosted-login-only로 범위를 명시한다.
      • Verify: P2B parent의 In/Out scope와 TODO를 확인한다.
  • Concrete failure mode: account-link button/status 요구가 있는데도 SPA change가 없다고 계획해 CIAL, CSRF state, linked-account read path가 누락된다.
  • Falsification condition: 제품 요구가 Keycloak-hosted login page에서 Google을 선택하는 것뿐이고 account UX를 제공하지 않으면 zero-change는 성립한다.
  • Required action: 제목·목표를 “Google IdP sign-in entry zero-change”로 좁히고 account-link 기능별 change matrix를 추가한다.
  • Why this action: brokering의 login-path 불변식과 선택적 account UX 비용을 함께 보존한다.
  • Alternatives considered:
    1. Hosted login only — SPA diff 0.
    2. idpHint 버튼 — 작은 SPA 변경으로 Google을 직접 선택한다.
    3. Account Console 위임 — SPA account-link 코드는 줄지만 별도 UX로 이동한다.
    4. In-SPA linking/status — CIAL과 read API/backend 구현이 필요하다.
  • Counterargument: 이 branch는 :53에서 Account Linking을 명시적으로 제외한다. finding은 branch 내부 구현이 아니라 제목·목표가 상위 P2B 범위로 일반화될 위험을 지적한다.
  • Verification command and observed output:
$ grep -nFo -- '**P2A 코드 그대로 두고 Keycloak에 Google IdP만 추가**해서 SPA가 변경 없이 Google 로그인 가능함을 실증한다.' 'raw/branch-notes/feature-keycloak-federation-spa-zero-change.md'
31:**P2A 코드 그대로 두고 Keycloak에 Google IdP만 추가**해서 SPA가 변경 없이 Google 로그인 가능함을 실증한다.

$ grep -nFo -- 'SPA 가 link 를 트리거하는 공식 메커니즘 = Client Initiated Account Linking (서명된 redirect URL fabrication)' 'raw/branch-notes/feature-keycloak-account-linking-spa-ux.md'
105:SPA 가 link 를 트리거하는 공식 메커니즘 = Client Initiated Account Linking (서명된 redirect URL fabrication)

$ grep -nFo -- 'Account Linking 정책 — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]' 'raw/branch-notes/feature-keycloak-federation-spa-zero-change.md'
53:Account Linking 정책 — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]

L1-F14: zero-change token baseline에서 aud owner와 기대값이 모호하다

  • Source file: raw/branch-notes/feature-keycloak-federation-spa-zero-change.md
  • Source quote:

    aud=<spa-client-id> 확인

  • Counterpart quote:

    aud=backend/SPA client, azp=SPA client → 구조 동일

  • Source lines: raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:78, raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:131
  • Severity: High
  • Classification: FACT — ambiguous default; P2A audience owner가 unseen이므로 NEEDS_CROSS_LANE_AUDIT.
  • Claim: 동일 token expectation에서 aud가 SPA client로 고정되기도 하고 backend 또는 SPA client로 열려 있기도 하다.
  • Real-world assumptions:
    1. SPA와 Resource API가 별도 clients이고 backend가 audience를 강제한다.
      • Falsifies if: 하나의 client가 SPA와 API audience를 겸하도록 의도됐다.
      • Verify: realm export의 client/audience mapper와 backend validator를 확인한다.
  • Concrete failure mode: token aud가 SPA client뿐인데 backend가 API audience를 기대하면 401이 발생한다. 반대로 backend가 SPA audience를 수용하면 cross-client token reuse 경계가 약해질 수 있다.
  • Falsification condition: P2A owner가 aud=<spa-client-id>를 명시적으로 선택했고 backend validator도 같은 값을 SSOT로 참조한다면 모호성은 해소된다.
  • Required action: P2A audience owner D-row를 직접 참조하고 expected claims를 iss, aud, azp 각각 정확한 client ID로 고정한다.
  • Why this action: zero-change 검증은 기준선이 한 값으로 정의돼야 의미가 있다.
  • Alternatives considered:
    1. aud=api-client, azp=spa-client — SPA/API 분리가 선명하다.
    2. aud=spa-client — 단일-client 학습에는 단순하지만 backend trust를 명시해야 한다.
    3. 복수 audience — 여러 API에 유연하지만 token scope가 넓어진다.
    4. BFF audience — BFF가 token holder인 별도 pattern이다.
  • Counterargument: backend/SPA client가 환경별 placeholder를 뜻할 수 있다. 그렇다면 zero-change test가 실제 어떤 값을 assert할지 선택 조건을 추가해야 한다.
  • Verification command and observed output:
$ grep -nFo -- '`aud=<spa-client-id>` 확인' 'raw/branch-notes/feature-keycloak-federation-spa-zero-change.md'
78:`aud=<spa-client-id>` 확인

$ grep -nFo -- '`aud`=backend/SPA client, `azp`=SPA client → **구조 동일**' 'raw/branch-notes/feature-keycloak-federation-spa-zero-change.md'
131:`aud`=backend/SPA client, `azp`=SPA client → **구조 동일**

Grep verification

  • Findings: 14
  • Verbatim quotes: 30
  • 실행한 quote-specific grep -nFo: 30
  • 비어 있지 않은 관측 결과: 30
  • Line corrections: 0
  • Dropped quotes/findings: 0
  • 모든 finding의 source/counterpart quote가 위 명령 결과와 일치했다.

Claim Traceability Check

  • Claim ID 연결 검사: 10개 branch-note의 DEM 구조를 읽었다. 같은 lane에 counterpart가 있는 decision edge는 의미 대조했으며, raw claim source body와 unseen owner D-row는 고정 slice 밖이므로 BROKEN_CLAIM_REFERENCE 여부를 판정하지 않았다.

  • Decision Evidence Map: 10개 검토. lane-local semantic drift는 L1-F01~L1-F14에 반영했다.

  • UNSUPPORTED_DECISION: 표기 자체만으로 inconsistency로 올리지 않았다. categorical summary/current DEM/owner summary가 어긋난 경우만 finding으로 다뤘다.

  • Repository changes: 없음. 읽기 전용.

Cross-Lane Candidates

Citing file:line Referenced owner/decision Status One-line reason
raw/branch-notes/feature-authentication-authorization-contract.md:124,219 feature-security-operational-baselineJwtToAuthenticatedUserConverter, AUTHZ code SSOT NEEDS_CROSS_LANE_AUDIT ROLE_* authority와 raw role seam의 실제 owner vocabulary 확인 필요.
raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:181 feature-keycloak-first-broker-login-flow D1/D2 NEEDS_CROSS_LANE_AUDIT auto-link disable과 Email/Re-auth execution profile의 authoritative D-row 확인 필요.
raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:183 feature-keycloak-idp-mappers-claim-to-role D2 NEEDS_CROSS_LANE_AUDIT email_verified가 custom gate인지 Trust Email 설정인지 owner 판정 필요.
raw/branch-notes/feature-keycloak-account-linking-spa-ux.md:184 feature-keycloak-google-claim-attribute-mapping D1/D4 NEEDS_CROSS_LANE_AUDIT sub/email claim mapping과 collision input의 owner summary 확인 필요.
raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:211 feature-keycloak-first-broker-login-flow D1/D2/D4 NEEDS_CROSS_LANE_AUDIT password/email verification 및 email_verified=false 방어 flow가 unseen owner에 위임됨.
raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:212 feature-keycloak-google-claim-attribute-mapping D3/D4 NEEDS_CROSS_LANE_AUDIT Sync Mode와 email Attribute Importer의 current owner 결정을 확인해야 함.
raw/branch-notes/feature-keycloak-bff-vs-spa-direct.md:226-229 P2A parent D1, token-storage D1/D5, refresh D1/D2, audience D1/D4 NEEDS_CROSS_LANE_AUDIT SPA Direct 기준선 네 축이 본 lane 밖 owner에 의존함.
raw/branch-notes/feature-keycloak-docker-compose-stack.md:236,240 feature-keycloak-single-ec2-no-google D3; feature-keycloak-iss-claim-hostname-mismatch NEEDS_CROSS_LANE_AUDIT public issuer/internal reachability contract의 authoritative network profile 확인 필요.
raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:252 root feature-keycloak-patterns F3/F4 NEEDS_CROSS_LANE_AUDIT shared realm/client와 secret ownership이 P1B Google client 설계 전제임.
raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:253 idp-client, first-broker, claim-mapping child owners NEEDS_CROSS_LANE_AUDIT parent D1-D5와 child owner 경계를 전체 hierarchy에서 확인해야 함.
raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:248 feature-keycloak-header-spoofing-defense D1/D3/D4 NEEDS_CROSS_LANE_AUDIT header-only trust의 load-bearing network isolation 결정이 unseen owner에 있음.
raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:249-250 oauth2-proxy flow D2; nginx auth-request D2/D3/D5 NEEDS_CROSS_LANE_AUDIT P1A hub가 세부 config를 재진술했는지 owner D-row와 의미 대조 필요.
raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:145 feature-keycloak-internal-spa-direct-no-google D3 NEEDS_CROSS_LANE_AUDIT iss/signature/expiry/audience baseline과 정확한 aud 값을 확인해야 함.
raw/branch-notes/feature-keycloak-federation-spa-zero-change.md:146-149 P2B parent, first-broker, claim-mapping, three-leg-trust owners NEEDS_CROSS_LANE_AUDIT zero-change 전제가 현재 delegated owners의 D-row와 맞는지 확인 필요.
agent: wiki-research-lane
found: 10
processed: 10
dropped: 0