277 lines
26 KiB
Markdown
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`)
|