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

26 KiB

title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, parent_branch, related_projects, tags, created, target_merge, status_label, contract_packet_sha256
title source_type status id kind project work_item inherits refines overrides depends_on contract_packet branch parent_branch related_projects tags created target_merge status_label contract_packet_sha256
branch / feature-keycloak-account-linking-spa-ux (Account Linking 정책 — SPA 컨텍스트의 사용자 노출) branch-note raw BR-KEYCLOAK-CHILD-1666E2E0 branch-child keycloak-patterns-overview WI-KEYCLOAK-PATTERNS-OVERVIEW-020
DEC-KEYCLOAK-PATTERNS-OVERVIEW-AUTH-TAXONOMY-001@1
1 feature-keycloak-account-linking-spa-ux feature-keycloak-patterns
keycloak-patterns
branch
keycloak-patterns
spa
idp-brokering
google-federation
account-linking
p2b
2026-05-25 in-progress 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 단계.

부모 (필수)

raw/branch-notes/feature-keycloak-patterns

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: project governance hub가 AP1~AP4 taxonomy와 child progress index를 유지한다

상속한 프로젝트 결정

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

브랜치 지역 결정

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

선언한 예외

Override ID Overrides Reason Approval Status

없음.

목표

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:

범위

포함 범위

  • 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" 메뉴

제외 범위

근거 (필수, 최소 1개+)

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 재framingemail_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 Claimsraw/<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).

시나리오 동작 근거
신규 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)

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 캡처용. 정상 경로 외 실패/엣지/의존.

검증해야 할 주장

공식 문서 근거가 있어도 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.

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의 audaccount가 포함되어야 함. Keycloak 기본 client 설정에서 account audience가 자동 포함되는지 확인 필요.

묶음

본 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)