Files
llm-wiki/raw/branch-notes/feature-keycloak-account-linking-spa-ux.md

277 lines
26 KiB
Markdown

---
title: branch / feature-keycloak-account-linking-spa-ux (Account Linking 정책 — SPA 컨텍스트의 사용자 노출)
source_type: branch-note
status: raw
id: BR-KEYCLOAK-CHILD-1666E2E0
kind: branch-child
project: keycloak-patterns-overview
work_item: WI-KEYCLOAK-PATTERNS-OVERVIEW-020
inherits: [DEC-KEYCLOAK-PATTERNS-OVERVIEW-AUTH-TAXONOMY-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
branch: feature-keycloak-account-linking-spa-ux
parent_branch: feature-keycloak-patterns
related_projects: [keycloak-patterns]
tags: [branch, keycloak-patterns, spa, idp-brokering, google-federation, account-linking, p2b]
created: 2026-05-25
target_merge:
status_label: in-progress
contract_packet_sha256: 90ab9a8505129356c2a13f017db9741ca882313952664fa7262177c192b79286
---
# branch: feature-keycloak-account-linking-spa-ux (Account Linking 정책 — SPA 컨텍스트)
> Layer: `raw/branch-notes/` — [[raw/branch-notes/feature-keycloak-patterns]]의 branch-child.
> 학습 노트. P2B는 `documented-only` 단계.
<!-- section-id: branch-parent -->
## 부모 (필수)
[[raw/branch-notes/feature-keycloak-patterns]]
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: project governance hub가 AP1~AP4 taxonomy와 child progress index를 유지한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-KEYCLOAK-PATTERNS-OVERVIEW-AUTH-TAXONOMY-001@1` | canonical 분류축은 AP1~AP4 인증 통합 아키텍처와 cross-cutting 변형이다 | account-linking SPA UX를 IdP-brokering cross-cutting 학습 자료로 분류한다 | [[raw/project-notes/keycloak-patterns-overview]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
| D1 | automatic email linking 대신 confirm flow를 사용한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
| D2 | SPA UX는 linking trust policy owner의 결론을 소비한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
| D3 | link 상태 표시는 Account REST API 검증 대상으로 둔다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
| D4 | unlink UX는 Account Console을 기본 진입점으로 둔다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
| D5 | link trigger는 Client Initiated Account Linking을 사용한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
없음.
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
P1B의 Account Linking 보안 정책(`sub` primary key + Confirm Link Existing Account)을 **SPA 컨텍스트에서** 어떻게 사용자에게 노출할지 정리. P1B는 oauth2-proxy가 cookie session을 다루지만, P2B는 SPA가 직접 token을 다루므로 **UX 노출 지점이 다름**.
면접 질문: "기존 Keycloak 사용자가 나중에 Google 로그인을 추가하려면 어떤 흐름인가요?"
→ "Keycloak의 Account Console에서 'Linked Accounts' 메뉴를 통해 Google 계정을 link합니다. 또는 로그인 화면에서 Google로 처음 로그인했을 때 같은 email의 기존 계정이 있으면 'Confirm Link Existing Account' 뒤에 owner flow가 선택한 Email verification 또는 password re-authentication이 실행됩니다. SPA는 이 흐름에 직접 관여하지 않고, Keycloak이 redirect로 처리합니다. SPA는 link 완료 후 access token을 받고, 자체 UI로 'Google 계정 연결됨'을 표시할 수 있습니다 (token claim 또는 별도 API 호출)."
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- **3가지 사용자 시나리오**:
1. **신규 Google 사용자** (Keycloak에 같은 email 없음) → First Broker Login Flow가 자동 user 생성
2. **기존 Keycloak local 사용자**가 Google 로그인 시도 (같은 email) → Confirm Link Existing Account → owner flow의 verification profile(Email 또는 password re-authentication) 통과 후 link
3. **이미 link된 사용자** → 평상 로그인
- Account 관리 진입점 비교 (SPA 관점):
- Keycloak Account Console (`/realms/{r}/account/`) — Keycloak이 제공하는 user-facing UI
- SPA 자체 UI + Keycloak Admin REST API
- Keycloak Account REST API (사용자 자신의 데이터 조작)
- SPA가 "Account linked"를 표시하는 방법
- access token claim에 `federated_identities` 포함 불가 (기본). Account REST API 호출 또는 별도 backend endpoint.
- unlink 흐름 — Keycloak Account Console의 "Linked Accounts" 메뉴
### 제외 범위
- 코드 변경 없음 검증 — [[raw/branch-notes/feature-keycloak-federation-spa-zero-change]]
- claim mapping — [[raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role]]
- JWT signature 검증 — [[raw/branch-notes/feature-keycloak-three-leg-trust-chain]]
- P1B와의 흐름 동일성은 [[raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation]] (Edge proxy 컨텍스트) 참고
## 근거 (필수, 최소 1개+)
- [[raw/official-docs/keycloak-first-login-flow]] — D1 근거: email 자동 link 는 security hole 공식 경고 + Confirm Link info page 동작
- [[raw/official-docs/keycloak-client-initiated-account-linking]] — D5 근거: SPA/client 가 계정 링크를 트리거하는 공식 메커니즘("Client Initiated Account Linking" — 서명된 redirect URL fabrication + `account.manage-account`/`account.manage-account-links` role) + `keycloak.login({action:'link'})` built-in 가정 미확인(does-not-prove)
- [[raw/official-docs/google-oidc-discovery-spec]] — D2 위임 컨텍스트: Google `sub` 은 unique/never-reused(`GOOGLE-OIDC-C6`) → email 아닌 `sub` 기반 매칭 근거. [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D4 — linking trust policy를 consume한다.
## TODO
- [ ] First Broker Login Flow의 default authenticator 흐름 정리 — 등급: `documented-only`
- [ ] **Auto-Link 보안 위험** — default `Automatically Set Existing User` 사용 시 hijack 가능. 본 패턴은 사용 금지 — 등급: `documented-only`
- [ ] **Confirm Link Existing Account authenticator** 채택 — 후속 verification 방식은 [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D2 — owner profile을 consume — 등급: `documented-only`
- [ ] 신규 Google 사용자 first-time 경험: review profile (옵션) → 자동 user 생성 → SPA로 redirect — 등급: `documented-only`
- [ ] 기존 사용자 link 시점 — login 화면에서 자동 트리거 vs Account Console에서 명시적 link — 등급: `documented-only`
- [ ] SPA가 "현재 link된 IdP 목록" 표시하려면 — Keycloak Account REST API (`GET /realms/{r}/account/linked-accounts`) 호출 필요 — 등급: `needs-confirmation`
- [ ] unlink 흐름 — Account Console "Linked Accounts" → Remove. unlink 후 해당 IdP로 로그인 시 다시 first broker login 흐름 — 등급: `documented-only`
- [ ] 로컬 비밀번호 없는 사용자가 마지막 federated identity를 unlink하면 Keycloak 엔진이 거부 — [[raw/branch-notes/feature-keycloak-account-linking-sub-vs-email]] D3 — source-grounded, 배포 release tag 재확인 필요 — 등급: `documented-only`
- [ ] SPA에서 link/unlink 트리거 시 redirect 흐름 — `keycloak.login({ action: 'link', idpHint: 'google' })` 가능 여부 — 등급: `needs-confirmation`**조사 반영(2026-07-15)**: 공식 메커니즘은 built-in login action 이 아니라 서명 redirect URL fabrication(D5, [[raw/official-docs/keycloak-client-initiated-account-linking]]). adapter helper 존재 여부만 잔여 확인.
## 진행 중 메모
- **P1B와의 핵심 차이**: P1B는 oauth2-proxy가 cookie session으로 사용자 상태를 가짐. SPA가 없으므로 "Account linked" UI는 별도 페이지(예: `/account/`)로 redirect. P2B는 SPA가 SPA 안에서 "내 계정" 화면을 그리고, link 상태는 API 호출로 가져옴.
- **하지만 brokering 흐름 자체는 동일** — Keycloak이 First Broker Login Flow를 실행하고, SPA / proxy는 결과만 받음.
- **Auto-Link의 위험** (재확인):
- 시나리오: 공격자가 `victim@example.com`로 Google 가입 (Google은 `email_verified=true` 표시) → 그 Google 계정으로 Keycloak 로그인 → 만약 Auto-Link면 `victim@example.com` Keycloak local 계정으로 자동 link → **계정 탈취**.
- 방어: `email_verified` 검증만으로는 부족하다. **Confirm Link Existing Account 뒤 소유 증명**이 본질이며, 구체 수단은 [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D2의 조건부 profile(Email 기본 또는 password 재인증 관철/폴백)을 따른다.
- **SPA가 link 상태를 표시하는 방법**:
1. Keycloak Account REST API `/realms/{r}/account/linked-accounts` 호출 (SPA가 자기 access token 사용 — `account` audience 필요)
2. 또는 backend가 Keycloak Admin API를 통해 `GET /admin/realms/{r}/users/{id}/federated-identity` 호출 후 SPA에 노출 (backend는 service account 사용)
- **unlink 후 재로그인**: link 해제하면 Federated Identity record가 삭제됨. 다시 Google로 로그인하면 First Broker Login Flow가 다시 실행 → Confirm Link Existing Account가 다시 트리거됨.
- **orphan account 방지**: [[raw/branch-notes/feature-keycloak-account-linking-sub-vs-email]] D3 — Keycloak 엔진의 마지막 federated identity 제거 guard를 consume한다. 본 branch는 HTTP 400 UX 처리만 소유하며, 배포 release tag 동일성은 `needs-confirmation`이다.
- **조사 반영(2026-07-15, `/branch-spec`)**:
- **Claim #4 정정** — keycloak-js 에 `keycloak.login({action:'link'})` 같은 built-in link action 은 KC-CIAL 로 확인 안 됨. 공식 경로는 앱이 `broker/{provider}/link` redirect URL 을 hash 서명과 함께 **직접 fabricate**(D5). keycloak-js/securing-apps 문서 모두 built-in link 메서드를 다루지 않음.
- **D2 재framing** — `email_verified` 는 Keycloak 의 *linking gate* 가 아니라 **Trust Email**(외부 IdP 계정 생성 시 verified 표시) 설정(Keycloak admin 문서, WebSearch 비아카이브). [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D4 — linking trust policy를 정본으로 참조하며, 이 branch는 메커니즘을 복제하지 않는다.
- **CVE 참고** — first-broker-login 의 cross-session email verification proof 가 upstream identity 에 bound 되지 않는 취약점(CVE-2026-9087, keycloak/keycloak#49175, 비아카이브)이 존재 → 실 구현 단계에서 Keycloak 버전 patch 확인 대상.
## 결정 사항
> 추후 면접/회고에서 "왜 이렇게 했나" 답할 근거. 대안과 함께 기록.
- 2026-05-25: **Auto-Link 금지 / Confirm Link Existing Account 채택** — 이유: 같은 email의 기존 계정 hijack 방지. (P1B와 동일 정책)
- 2026-05-25 (**Historical / superseded**): `email_verified=true`만 link 허용한다는 초기 판단. Active policy는 [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D4 — linking trust policy를 참조한다.
- 2026-05-25: **SPA의 link 상태 표시 = Keycloak Account REST API 호출** — backend 거치지 않고 SPA가 직접. 이유: backend 코드 추가 0 (P2A와 동일하게 유지). Account API audience(`account`)가 SPA token에 자동 포함되는지 확인 필요 (`needs-confirmation`).
- 2026-05-25: **unlink 흐름 = Keycloak Account Console 사용** — SPA 자체 UI는 학습 단계에선 미구현. 운영 시 SPA 안에 카드형 UI 추가 검토.
- 2026-07-15 (`/branch-spec` 조사): **D5 추가 — SPA 가 link 를 트리거하는 공식 메커니즘 = Client Initiated Account Linking (서명된 redirect URL fabrication)** — keycloak-js built-in login action 이 아님. 검토한 대안: (a) `keycloak.login({action:'link'})` built-in — 공식 근거 없음, (b) 앱이 `broker/{provider}/link?...&hash=` URL 직접 구성 — 공식(KC-CIAL). 근거: [[raw/official-docs/keycloak-client-initiated-account-linking]].
- 2026-07-15 (`/branch-spec` 재framing): **D2 는 이 브랜치가 소유하지 않고 위임(consume)** — [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D4 — linking trust policy. 이 브랜치는 SPA 컨텍스트에서 동일 게이트를 재진술하지 않고 참조만 한다(consistency-contract Single-Owner).
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다.
> `Decision ID` 는 이 branch-note 안에서 안정적으로 유지한다.
> `Supporting Claims` 는 `raw/<category>/<slug>.md#<CLAIM-ID>` 형식.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | Auto-Link 금지 / Confirm Link Existing Account 채택 (같은 email 의 기존 계정 hijack 방지) | 외부 IdP email 을 항상 신뢰 못할 때(=일반 케이스) → Confirm Link. 폐쇄망에서 IdP 를 완전 신뢰하면 Trust Email + auto-link 도 가능하나 본 패턴 미채택 | `raw/official-docs/keycloak-first-login-flow.md#KC-FLF-C2`, `raw/official-docs/keycloak-first-login-flow.md#KC-FLF-C3` — flow *구성*(정확한 authenticator step)은 owner [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D1(auto-link `DISABLED`)·D2(Confirm Link + Verify Existing Account — SMTP 시 Email 기본 / Re-auth 는 email authenticator DISABLE 시)에 위임 | `official-vendor-doc` (공식 경고 + Confirm Link info page 동작) | "Confirm Link Existing Account" 가 default flow 에 포함되는지 vs 별도 추가 필요한지 Keycloak version 별 확인 필요 (owner 브랜치 D2 에서 추적) |
| D2 | SPA UX는 linking trust policy를 새로 정하지 않고 owner 결론을 consume | 정책을 바꾸려면 owner에서 변경. 이 branch는 사용자 노출과 상태 표시만 다룸(Reference-Only) | [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D4 — linking trust policy | `delegated` | `Trust Email`은 linking gate가 아니다. custom SPI artifact가 확인되지 않은 상태에서 이 branch가 별도 hard-reject를 주장하지 않음 |
| D3 | SPA 의 link *상태 표시* = Keycloak Account REST API (`GET /realms/{r}/account/linked-accounts`) 직접 호출 | backend 없이 SPA 가 상태 표시해야 → Account REST API 시도(미문서화, `needs-confirmation`). backend 가 이미 있으면 → Admin API `federated-identity`(문서화, service account)가 안전 | UNSUPPORTED_DECISION | — | KC-CIAL(신규 Source)이 이 `linked-accounts` read endpoint 를 **다루지 않음을 명시 확인**(does-not-prove) + 커뮤니티상 undocumented(WebSearch). `account` audience 자동 포함 여부는 Claims To Verify 로 이관 |
| D4 | unlink UX 진입점 = Keycloak Account Console (SPA 자체 UI 미구현) — UX 진입점 선택만 소유 | 학습 단계 → 기본 Account Console UI(코드 0). 운영에서 in-SPA unlink UX 필요 → SPA 자체 카드 UI(별도 구현) | UNSUPPORTED_DECISION (UX 진입점 선택); [[raw/branch-notes/feature-keycloak-account-linking-sub-vs-email]] D3 — unlink 안전정책 owner | — | 엔진 guard는 source-grounded이나 인용 기준이 Keycloak `main`이므로 배포 release tag 재확인 필요. 본 branch는 400 UX만 결정 |
| D5 | SPA 가 link 를 **트리거**하는 방법 = Client Initiated Account Linking (앱이 서명된 redirect URL 을 fabricate), keycloak-js built-in login action 아님 | 로그인된 사용자에게 "지금 Google 연결" 버튼 제공 → client-initiated linking URL fabricate(D5). 최초 로그인 시 자동 link 은 First Broker Login(D1) 경로 — 트리거 시점이 다름 | `raw/official-docs/keycloak-client-initiated-account-linking.md#KC-CIAL-C1`, `raw/official-docs/keycloak-client-initiated-account-linking.md#KC-CIAL-C2`, `raw/official-docs/keycloak-client-initiated-account-linking.md#KC-CIAL-C3`, `raw/official-docs/keycloak-client-initiated-account-linking.md#KC-CIAL-C4` | `official-vendor-doc` | SPA(브라우저)가 hash 재료(`token.getSessionState()`/`token.getIssuedFor()`)를 keycloak-js `tokenParsed` 에서 얻는지 코드 미확인(`planned`). `KC-CIAL-C4` does-not-prove: 문서가 "CSRF 를 완전히 막지는 못한다" 경고 → SPA state 별도 방어 필요 |
## 구현 가이드
> *결정(Decisions)* 이 "*무엇*" 이라면 본 §는 "*어디에 어떻게*" 의 사전 명세. 본 브랜치는 `documented-only` 학습 노트이므로, 구현 detail 은 **공식 문서가 규정하는 프로토콜·값**을 anchor 로 하고 코드 미확인 항목은 `planned`/`UNSUPPORTED_IMPL_DECISION` 로 명시한다.
### 1. 계정 링크 트리거 — Client Initiated Account Linking (SPA → Keycloak)
> **Trace**: D5 + `KC-CIAL-C1`/`C2`/`C3`/`C4`. 이미 로그인된 SPA 사용자가 "Google 연결" 버튼을 눌렀을 때의 사전 명세.
>
> - **UNSUPPORTED_IMPL_DECISION**: (a) SPA(브라우저)가 hash 재료 `token.getSessionState()`·`token.getIssuedFor()` 를 keycloak-js `tokenParsed.session_state`·`tokenParsed.azp` 로 얻는 매핑 — KC-CIAL 예시는 Java Servlet 전용(`KC-CIAL-C3` does-not-prove), JS 매핑은 미확인. trade-off: 학습 단계엔 이 매핑을 `planned` 로 남기고 실 구현 시 keycloak-js `tokenParsed` 필드로 검증. (b) 브라우저에서 SHA-256 계산은 `crypto.subtle.digest('SHA-256', ...)` + Base64URL 인코딩으로 수행 — 표준 Web Crypto 이나 KC-CIAL 이 JS 구현을 규정하지 않으므로 `UNSUPPORTED_IMPL_DECISION`.
| 항목 | 값 / 명세 | 근거 |
|---|---|---|
| redirect URL 템플릿 | `{auth-server-root}/auth/realms/{realm}/broker/{provider}/link?client_id={id}&redirect_uri={uri}&nonce={nonce}&hash={hash}` | `KC-CIAL-C3` |
| `provider` | `google` (Keycloak IdP alias) | D5, `KC-CIAL-C3` (provider 파라미터) |
| `hash` 계산 | Base64URL( SHA_256( `nonce` + `token.getSessionState()` + `token.getIssuedFor()` + `provider` ) ) | `KC-CIAL-C4` |
| 전제조건 | 사용자에 `account.manage-account` 또는 `account.manage-account-links` role + 그 role 의 scope 가 access token 에 부여 + 앱이 자기 access token 접근 | `KC-CIAL-C2` |
| 목적(왜 hash) | auth server 가 client 가 요청을 시작했음을 보장(rogue app 임의 link 방지) — 단 CSRF 완전 방지는 아님 | `KC-CIAL-C4` |
### 2. 최초 로그인 링크 경로 — First Broker Login (Google 로그인 화면 → Keycloak)
> **Trace**: D1 + `KC-FLF-C2`/`C3`/`C4`. §1 과 다른 트리거 시점(로그인 시 email collision).
>
> - **OUT_OF_BRANCH_SCOPE**: [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D2 — Confirm Link 이후 verification profile의 정본. 이 브랜치는 owner 결과에 따른 UX만 기술한다.
| 시나리오 | 동작 | 근거 |
|---|---|---|
| 신규 Google 사용자(같은 email 없음) | 자동 user 생성. Review Profile mode 에 따라 확인 페이지(`On`/`missing`/`Off`) | `KC-FLF-C4` |
| 같은 email 기존 사용자 | Confirm Link Existing Account info page → (A) profile 재검토 후 다른 email/username, (B) 기존 계정 link 확인 | `KC-FLF-C3` |
| 금지 | `Automatically Set Existing User`(auto-link) — email 자동 link 는 security hole | `KC-FLF-C2` |
### 3. 링크 상태 표시 (Read path)
> **Trace**: D3 (UNSUPPORTED_DECISION). SPA 가 "현재 연결된 IdP" 를 그리는 방법.
>
> - **UNSUPPORTED_IMPL_DECISION**: 옵션 A 의 `/account/linked-accounts` endpoint 는 KC-CIAL 가 다루지 않고 커뮤니티상 undocumented. trade-off: **backend-zero(옵션 A, 미검증)** vs **문서화된 안정성(옵션 B, backend 코드 추가)**. 학습 단계엔 옵션 A 를 `planned` 로 시도하되 실패 시 옵션 B fallback.
| 옵션 | 호출 | 인증 | 상태 |
|---|---|---|---|
| A | SPA → `GET /realms/{r}/account/linked-accounts` | SPA access token (`account` audience 필요) | `needs-confirmation` (undocumented) |
| B | backend → `GET /admin/realms/{r}/users/{id}/federated-identity` | service account | `documented-only` (Admin API) |
### 4. Unlink
> **Trace**: D4 (UNSUPPORTED_DECISION, UX 진입점). [[raw/branch-notes/feature-keycloak-account-linking-sub-vs-email]] D3 — 안전정책 owner.
>
> - **UNSUPPORTED_IMPL_DECISION**: Account Console UI를 재사용할지 SPA 자체 unlink 카드를 만들지는 project UX 선택이다. orphan 거부 자체는 owner D3의 엔진 guard를 consume하며, 이 branch는 HTTP 400 안내를 처리한다.
## 엣지·실패·의존
> R4 캡처용. 정상 경로 외 실패/엣지/의존.
- **실패·엣지 경로**:
- **hash 누락/불일치 (§1)**: `broker/{provider}/link``hash` 가 없거나 틀리면 auth server 가 link 요청을 client-initiated 로 인정하지 않음(`KC-CIAL-C4`). 단 문서가 "CSRF 를 완전히 막지 못함" 경고 → SPA 는 별도 `state` 로 CSRF 방어 필요.
- **Auto-Link hijack (§2)**: 공격자가 `victim@example.com` 로 Google 가입 후 그 IdP 로 로그인 → auto-link 면 계정 탈취. 방어 = D1 Confirm Link + [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D2의 owner-selected verification profile(Email 기본 또는 password 재인증 관철/폴백); `email_verified` 만으로는 불충분(`KC-FLF-C2`).
- **orphan account (§4)**: [[raw/branch-notes/feature-keycloak-account-linking-sub-vs-email]] D3 — 마지막 federated identity 제거 guard를 consume. 배포 release tag가 owner 근거와 같은지는 `needs-confirmation`.
- **unlink 후 재로그인 (§4)**: Federated Identity record 삭제 → 다시 Google 로그인 시 First Broker Login 재실행 → Confirm Link 재트리거(D1 경로).
- **`account` audience 부재 (§3)**: SPA access token 의 `aud``account` 미포함 시 옵션 A 는 401/403 → 옵션 B fallback 필요.
- **CVE-2026-9087 (§2)**: first-broker-login 의 cross-session email verification proof 가 upstream identity 에 bound 안 됨(keycloak/keycloak#49175, 비아카이브). 실 구현 시 Keycloak 버전 patch 상태 확인.
- **다른 계약 의존**:
- [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D1 — silent auto-link 방지 owner. [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D2 — verification profile owner.
- [[raw/branch-notes/feature-keycloak-account-linking-sub-vs-email]] D1 — persistent federation key owner. [[raw/branch-notes/feature-keycloak-account-linking-sub-vs-email]] D3 — unlink guard owner.
- [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D4 — linking trust policy; 본 branch는 UX만 consume.
- [[raw/branch-notes/feature-keycloak-google-claim-attribute-mapping]] D4 — Google claim→user attribute mapping owner.
## 검증해야 할 주장
> 공식 문서 근거가 있어도 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| "Confirm Link Existing Account" authenticator 가 P2B 의 default first-broker-login flow 에 자동 포함된다 | KC-FLF-C3 는 authenticator 의 동작만 정의하며, default flow inclusion 여부는 Keycloak version (26.x) 별 admin UI 에서 확인 필요 | Keycloak admin console > Authentication > Flows > First Broker Login 의 step list 캡처 | `needs-confirmation` |
| SPA access token 의 `aud` claim 에 `account` audience 가 자동 포함되어 Account REST API 호출 가능 | 본 branch 의 Sources 에 audience mapping 동작 문서 없음. Keycloak default client 설정 의존 | dev 환경에서 SPA access token decode 후 `aud` 필드 확인 + `GET /realms/{r}/account/linked-accounts` 호출 결과 200 확인 | `needs-confirmation` |
| Keycloak Account REST API 에 `linked-accounts` read endpoint 가 실제로 존재하고 SPA 로 호출 가능하다 | KC-CIAL 은 이 endpoint 를 다루지 않음(does-not-prove); 커뮤니티상 undocumented(WebSearch) | dev 환경에서 실 호출 + 응답 schema 확인, 또는 옵션 B(Admin API `federated-identity`)로 대체 | `needs-confirmation` |
| 인용한 Keycloak 엔진 unlink guard가 배포 release tag에서도 동일하게 동작한다 | [[raw/branch-notes/feature-keycloak-account-linking-sub-vs-email]] D3의 근거는 Keycloak `main` source이며 배포 image tag와 동일성은 아직 확인하지 않음 | 배포 tag의 `LinkedAccountsResource` guard 대조 + Google-only 사용자로 마지막 link 제거 시 HTTP 400 확인 | `needs-confirmation` (owner decision은 source-grounded) |
| SPA 에서 link 트리거는 built-in `keycloak.login({action:'link'})` 가 아니라 `broker/{provider}/link` 서명 URL fabrication 이다 | KC-CIAL(`KC-CIAL-C1`/`C3`)은 앱이 URL 을 직접 fabricate 함을 규정하나 adapter API 표면은 다루지 않음 — keycloak-js 가 helper 를 제공하는지는 미확인 | keycloak-js 공식 adapter 레퍼런스에서 `login()` action 지원 목록 확인 + dev 환경에서 서명 URL 수동 구성 테스트 | `needs-confirmation` |
## 마주친 문제
- (학습 단계, 미실행)
- **잠재적 함정**: SPA가 Account REST API를 호출할 때 access token의 `aud``account`가 포함되어야 함. Keycloak 기본 client 설정에서 `account` audience가 자동 포함되는지 확인 필요.
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/official-docs/keycloak-client-initiated-account-linking]]
- [[raw/official-docs/keycloak-first-broker-login-flow]]
- [[raw/official-docs/keycloak-first-login-flow]]
<!-- GENERATED: sources:end -->
> 본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
### 오류 기록 (이 sub-sub-branch 작업 중 발생)
- (없음 — 현재 documented-only 단계)
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (없음 — Phase 3 실 구현 단계에 누적)
## 관련 일일 노트
## 완료 후 정리
> 학습 노트. P2B는 `documented-only` 유지.
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경: 학습 노트 (`documented-only`)
- **wiki 추출 대상**:
- `actually-implemented` 항목: (없음)
- `locally-verified` 항목: (없음)
- `prod-verified` 항목: (없음)
- **추출하지 않을 항목**: 전 항목 (`documented-only` / `needs-confirmation`)