332 lines
33 KiB
Markdown
332 lines
33 KiB
Markdown
---
|
|
title: branch / feature-keycloak-account-linking-sub-vs-email (Account Linking 보안 — sub vs email)
|
|
source_type: branch-note
|
|
status: raw
|
|
id: BR-KEYCLOAK-PATTERNS-OVERVIEW-018
|
|
kind: project-work-item
|
|
project: keycloak-patterns-overview
|
|
work_item: WI-KEYCLOAK-PATTERNS-OVERVIEW-018
|
|
inherits: [DEC-KEYCLOAK-PATTERNS-OVERVIEW-IDP-BROKERING-001@1]
|
|
refines: []
|
|
overrides: []
|
|
depends_on: [WI-KEYCLOAK-PATTERNS-OVERVIEW-016]
|
|
contract_packet: 1
|
|
branch: feature-keycloak-account-linking-sub-vs-email
|
|
parent_branch:
|
|
related_projects: [keycloak-patterns]
|
|
tags: [branch, keycloak-patterns, p1b, account-linking, security, account-takeover]
|
|
created: 2026-05-25
|
|
target_merge:
|
|
status_label: in-progress
|
|
contract_packet_sha256: 54e36081984bb3b7bb0b46f7bd31beb8a7f6ff170e7c6ca76c76f4d0228bf27d
|
|
---
|
|
|
|
# branch: feature-keycloak-account-linking-sub-vs-email (Account Linking 보안 — sub vs email)
|
|
|
|
> Layer: `raw/branch-notes/` — [[raw/project-notes/keycloak-patterns-overview]] Work Item의 직접 자식 branch.
|
|
> 학습 노트: P1B는 `documented-only` (실 구현 안 함).
|
|
> `status_label`: `in-progress`
|
|
|
|
<!-- section-id: branch-parent -->
|
|
## 부모 (필수)
|
|
|
|
[[raw/project-notes/keycloak-patterns-overview]]
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: sub와 email linking key의 security comparison과 선택이 기록된다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| Decision Ref | Project Summary | Branch Application | Source |
|
|
|---|---|---|---|
|
|
| `DEC-KEYCLOAK-PATTERNS-OVERVIEW-IDP-BROKERING-001@1` | Google federation은 Keycloak IdP brokering과 hardened First Broker Login을 사용한다 | federated identity의 persistent key와 account-linking security policy에 적용한다 | [[raw/project-notes/keycloak-patterns-overview]] |
|
|
|
|
<!-- section-id: branch-local-decisions -->
|
|
### 브랜치 지역 결정
|
|
|
|
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
|
|---|---|---|---|---|
|
|
| D1 | federated identity의 persistent key로 Google sub를 사용한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
|
|
| D2 | 기존 local 계정 linking 시 재인증 조건을 고정한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
|
|
| D3 | self-service unlink lockout을 server guard와 UX로 처리한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
|
|
| D4 | self-service link의 추가 password 재확인은 근거 확보 전 보류한다 | `local` | [[raw/project-notes/keycloak-patterns-overview]] | `proposed` |
|
|
| D5 | attribute Sync Mode 선택은 별도 owner에 위임한다 | `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 -->
|
|
## 목표
|
|
|
|
Google federation 운영의 가장 큰 보안 함정은 **Account Linking의 primary key 선택**이다. email 기반 linking은 직관적이지만, 다음 두 사실 때문에 account takeover 시나리오를 만든다.
|
|
|
|
1. **email은 변경 가능**: Google 계정 소유자가 primary email을 변경할 수 있음.
|
|
2. **email은 재사용 가능**: Google Workspace에서 퇴사자 email이 신규 직원에게 재배정될 수 있음 ([[raw/company-tech-blogs/keycloak-google-login-codemancers]] 명시).
|
|
3. **`email_verified=false`인 Google 사용자 존재**: 일부 케이스에서 Google이 미인증 email로 ID token 발급 가능.
|
|
|
|
반면 **`sub` claim은 영구·불변**이며 Google이 사용자별로 발급한 globally unique ID. Account linking의 primary key는 반드시 `sub`여야 한다.
|
|
|
|
본 노트는 takeover 시나리오를 정리하고, sub 기반 linking 정책을 명시한다.
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- Account linking primary key 선택 (`sub` vs email) 및 그 근거 — takeover 위협 모델 A/B/C (본 branch 의 **core owned 결정 D1**).
|
|
- self-service unlink 의 lockout-safe 정책 — password 미설정 계정 보호 (본 branch owned **D3**; [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]] D4 가 안전정책을 본 branch 에 위임).
|
|
- 기존 local 계정에 IdP link 시 재인증 *정책 수준* 요구 (D2 — flow *구성* 은 sibling 위임).
|
|
- Sync Mode 가 takeover 안전성에 미치는 영향 *분석* (D5 — 직교성 확인; 선택 자체는 sibling 위임).
|
|
|
|
### 제외 범위
|
|
|
|
> 의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.
|
|
|
|
- 실제 Keycloak 구성·코드 구현 (본 sub-sub-branch 는 `documented-only` 학습 노트).
|
|
- First Broker Login Flow 의 authenticator step 값 구성 → [[raw/branch-notes/feature-keycloak-first-broker-login-flow]].
|
|
- Google claim → attribute mapper 구성 및 Sync Mode 값 선택 → [[raw/branch-notes/feature-keycloak-google-claim-attribute-mapping]].
|
|
- SPA link/unlink UX 진입점 및 client-initiated linking 프로토콜 → [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]].
|
|
- **sub-only 충돌 감지 authenticator** — OOTB `Create User If Unique` 는 email/username 매칭(`KC-FBLVERIFY-C5`)이므로 sub-only 매칭엔 커스텀 authenticator 필요. 별도 branch 대상 (§Audit & Findings 이관 권고).
|
|
- 비-Google IdP / SAML federation.
|
|
|
|
## 근거 (필수, 최소 1개+)
|
|
|
|
- [[raw/company-tech-blogs/keycloak-google-login-codemancers]] — 실무 사례: email 재배정 takeover 시나리오 + sub 기반 linking 권장 (`company-case-study` — corroboration 전용).
|
|
- [[raw/official-docs/keycloak-first-login-flow]] — Confirm Link Existing Account flow 공식 (auto-link = security hole).
|
|
- [[raw/official-docs/keycloak-first-broker-login-verify-authenticators-official]] — D2 정정 근거: "Verify Existing Account By Email" (SMTP 설정 시 `ALTERNATIVE` 기본값) vs "Verify Existing Account By Re-authentication" (email authenticator 사용 불가 시 fallback) 의 정확한 트리거 조건. 재인증은 기본값이 아니며, 강제하려면 관리자가 email authenticator 를 명시적으로 disable 해야 함.
|
|
- [[raw/official-docs/google-openid-connect-oidc]] — Google `sub` claim의 영구성 + `email_verified` 의미 ("Always use the sub field").
|
|
- [[raw/official-docs/keycloak-identity-brokering-overview-official]] — federated identity 모델.
|
|
- [[raw/official-docs/keycloak-account-console-unlink-lockout-guard-official]] — 엔진 소스 코드: Account Console self-service unlink 가 마지막 federated identity 제거를 password 미설정 시 HTTP 400 으로 거부하는 lockout guard (D3 근거).
|
|
- [[raw/official-docs/keycloak-identity-provider-sync-mode-official]] — D5 (Sync Mode = IMPORT) 공식 근거 (IMPORT/FORCE/LEGACY/INHERIT verbatim). Sync Mode 는 attribute 최신성만 다루며 linking key(`sub`) 안전성과는 무관함을 명시.
|
|
- [[raw/official-docs/keycloak-client-initiated-account-linking]] — D4 반대 근거: self-service link 전제 = role(`account.manage-account-links`) + access token 만 (`KC-CIAL-C2`), password 재확인 미언급.
|
|
|
|
## TODO (과거 계획 스냅샷)
|
|
|
|
각 항목 옆에 증거 등급. 아래는 2026-05-25 계획 스냅샷이며 active 정책은 §Decision Evidence Map을 따른다. 본 P1B sub-sub-branch는 전체 `documented-only` / `planned`.
|
|
|
|
- [ ] Takeover 시나리오 (A/B/C) 다이어그램화 — 등급: `planned`
|
|
- [ ] Keycloak federated identity 테이블 스키마 확인 — 등급: `needs-confirmation`
|
|
- 테이블명: `FEDERATED_IDENTITY`
|
|
- composite key: `(IDENTITY_PROVIDER, FEDERATED_USER_ID, USER_ID)` 추정 — 확인 필요
|
|
- [ ] sub 기반 linking 강제 정책 명시 — 등급: `documented-only`
|
|
- email 기반 자동 linking 금지 (앞선 -2-2 노트와 연결)
|
|
- federated identity primary key = Google `sub` claim
|
|
- [ ] 기존 Keycloak 계정에 federated identity link 시 password 재확인 정책 — 등급: `documented-only`
|
|
- "Confirm Link Existing Account" + "Verify Existing Account by Re-authentication" REQUIRED
|
|
- 사용자가 기존 계정 password를 입력해야 link 완료
|
|
- [ ] Account Console에서 사용자 link/unlink 정책 결정 — 등급: `documented-only`
|
|
- Self-service unlink 허용 시: 사용자가 비밀번호 미설정 상태에서 unlink → 잠금 위험 (대안 로그인 수단 미보유) → 사전 password 설정 강제
|
|
- Self-service link 허용 시: 사용자가 Account Console에서 새 Google 계정 link → 같은 takeover 위험 → password 재확인 필수
|
|
- [ ] email 변경 시 user attribute 동기화 정책 — 등급: `documented-only`
|
|
- Sync Mode FORCE면 매 로그인마다 갱신
|
|
- Sync Mode IMPORT면 first login 시점만 → 이후 Google 측 변경 무시 (안정성 ↑, 최신성 ↓)
|
|
|
|
> ⚠️ **2026-07-15 조사 정정 (위 항목 전제 수정)**: password 재인증(D2)·unlink lockout(D3)·self-service link 재확인(D4)·Sync Mode(D5) 관련 전제 일부는 공식 문서·엔진 소스 조사로 수정됐다. 원 TODO 는 verbatim 보존하되, 정정 내용은 §Decision Evidence Map 의 Open Risk 열 + §Audit & Findings 를 따른다.
|
|
|
|
## 진행 중 메모
|
|
|
|
- [[raw/company-tech-blogs/keycloak-google-login-codemancers]]가 명시: **"전 직원 email이 신규 직원에게 재배정되는 케이스 → email 기반 linking은 위험"**. 본 노트의 핵심 출처.
|
|
- Keycloak 공식 문서가 "Confirm Link Existing Account" flow를 보안 기본값으로 권장하는 이유가 바로 본 노트의 시나리오들.
|
|
- 면접에서 받기 좋은 질문: "왜 sub를 primary key로 쓰나? email로 하면 안 되나?" — Scenario A/B로 답변 가능.
|
|
- 본 정책은 P1B 한정이 아닌 모든 Google federation 패턴(P2B, P3B)에 동일 적용.
|
|
|
|
## 계정 탈취 시나리오 분석
|
|
|
|
> 본 § 는 §결정 사항의 근거가 되는 위협 모델. 각 결정(특히 D1)이 어떤 공격을 막는지의 분석.
|
|
|
|
### Scenario A: email 재배정 (퇴사자 → 신규 직원)
|
|
|
|
1. `alice@company.com` (Google sub = `sub_A`)이 Keycloak 계정 보유, federated identity = `sub_A`.
|
|
2. Alice 퇴사 → IT 관리자가 Google Workspace에서 alice@company.com 계정 삭제.
|
|
3. 신규 직원 Bob에게 같은 `alice@company.com` email 재배정 (Google sub = `sub_B`).
|
|
4. Bob이 Google 로그인 시도 → Keycloak이 받은 ID token의 `sub` = `sub_B`.
|
|
5. **email 기반 linking이면**: Keycloak이 email match로 Alice의 기존 계정에 Bob을 link → **Bob이 Alice의 권한 + 데이터에 접근**.
|
|
6. **sub 기반 linking이면**: `sub_B`로 federated identity 검색 → 미존재 → 신규 user 생성 (또는 confirm flow). 안전.
|
|
|
|
### Scenario B: 자체 email 변경 (Google 계정 소유자)
|
|
|
|
1. `eve@gmail.com` (sub = `sub_E`)이 Keycloak에 신규 가입 (federated identity = `sub_E`).
|
|
2. Eve가 자신의 Google 계정 primary email을 `victim@gmail.com`으로 변경 (Google이 허용하는 시나리오 — alias 변경 등).
|
|
3. Keycloak에 이미 `victim@gmail.com`으로 가입된 별개 사용자 Victim 존재.
|
|
4. **email 기반 매 로그인 재확인이면**: Eve가 다음 로그인 시 Keycloak이 새 email로 Victim 계정에 link 시도 → takeover.
|
|
5. **sub 기반이면**: `sub_E`로 매핑된 Eve 계정 그대로 사용. email attribute만 갱신 (sync mode FORCE) 또는 그대로 (IMPORT). → **Sync Mode 는 takeover 방지에 관여하지 않음**; 방지 주체는 sub 기반 linking (§Audit RATIONALE_CORRECTION).
|
|
|
|
### Scenario C: email_verified=false
|
|
|
|
1. 공격자가 Google OAuth client를 자체 운영하면서 `email_verified=false`인 임의 email을 가진 사용자로 가장.
|
|
2. Keycloak `trustEmail=true`로 설정돼 있으면 email match로 기존 victim 계정에 link.
|
|
3. 해결: `trustEmail=false` + First Broker Login Flow에 `Confirm Link Existing Account` (이미 [[raw/branch-notes/feature-keycloak-first-broker-login-flow]]에서 다룸).
|
|
|
|
## 결정 사항 (과거 기록)
|
|
|
|
> 2026-05-25 초기 판단의 보존 영역이다. **Active 정본은 아래 Decision Evidence Map이며**, D2와 D5의 값·선택 조건은 각 owner를 참조한다.
|
|
|
|
- 2026-05-25: federated identity primary key = **Google `sub` claim 만**. email은 attribute일 뿐 link key 아님.
|
|
- 2026-05-25: 기존 Keycloak local 계정에 Google federated identity 추가 link 시 → 기존 계정 password 재인증 필수 ("Verify Existing Account by Re-authentication" REQUIRED).
|
|
- 2026-05-25: Account Console self-service unlink는 사용자가 password를 설정한 경우에만 허용 (잠금 방지).
|
|
- 2026-05-25: Self-service link 시점에도 기존 password 재확인 강제.
|
|
- 2026-05-25: ~~Sync Mode = IMPORT (first login만). Google 측 email 변경이 Keycloak으로 자동 전파되지 않음 → Scenario B 회피.~~ **Superseded by D5** — takeover와 Sync Mode는 직교하며 값 선택은 owner가 소유한다.
|
|
|
|
> **정정 메모 (2026-07-15 조사)**: 위 원 결정 중 D2/D3/D4/D5 는 공식 문서·엔진 소스 조사로 일부 전제가 수정됐다 — 원문은 위에 verbatim 보존하고, 수정 내용은 §Decision Evidence Map 의 Open Risk 열과 §Audit & Findings 에 기록한다 (CLAUDE.md §11: 사용자 작성 결정은 자동 rewrite 금지, 정합 권고만).
|
|
|
|
## 결정-근거 매핑
|
|
|
|
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다.
|
|
> `Decision ID` 는 이 branch-note 안에서 안정적으로 유지한다.
|
|
> `Supporting Claims` 는 `raw/<category>/<slug>.md#<CLAIM-ID>` 형식.
|
|
> `선택 조건` (R2): 이 조건일 때 이 결정 / 다른 조건이면 어떤 대안.
|
|
|
|
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|---|
|
|
| D1 | federated identity primary key = Google `sub` claim 만 (email 은 link key 아님) — **본 branch core owned** | 항상 이 결정 — `sub` 는 불변·유일, email 은 변경(GOIDC-C3)·재배정(Scenario A) 가능하므로 link key 부적격. 대안(email 기반 link)은 email 의 불변·비재사용이 IdP 계약으로 보장될 때만 — Google 은 명시적 불가 | `raw/official-docs/google-openid-connect-oidc.md#GOIDC-C3` ("Always use the sub field ... even if the user changes their email address"), `raw/company-tech-blogs/keycloak-google-login-codemancers.md` (tech-blog 사례 — corroboration 전용, 공식 best practice 단정 금지) | `official-vendor-doc` (Google) + `company-case-study` | Keycloak `FEDERATED_IDENTITY` 스키마가 sub 를 어떻게 저장하는지 본 Sources 직접 보장 안 함(→ Claims To Verify). **OOTB `Create User If Unique` 는 email/username 으로 충돌 감지**(`raw/official-docs/keycloak-first-broker-login-verify-authenticators-official.md#KC-FBLVERIFY-C5`) → sub-only 매칭엔 커스텀 authenticator 필요(§Audit OUT_OF_BRANCH_SCOPE) |
|
|
| D2 | 기존 Keycloak local 계정에 Google federated identity link 시 → 기존 계정 password 재인증 — **단 기본 동작 아님**: SMTP 설정 realm 은 "Verify Existing Account By Email" 이 기본 `ALTERNATIVE` 이므로 admin 이 명시 DISABLE 해야 password 재인증 실행 | security-first(secret 소유 증명) → email authenticator DISABLE + Re-authentication. SMTP 미설정 → Re-authentication 자동 폴백. 소비자 서비스(마찰·지원부담 우선) → Email 검증 기본값 유지 가능(단 secret 미증명) | `raw/official-docs/keycloak-first-login-flow.md#KC-FLF-C2` (auto-link=security hole), `raw/official-docs/keycloak-first-broker-login-verify-authenticators-official.md#KC-FBLVERIFY-C1` (Email = SMTP 시 기본), `#KC-FBLVERIFY-C2` (재인증 관철 = email DISABLE), `#KC-FBLVERIFY-C3` (Re-auth 폴백). flow *구성* owner = [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D2 | `official-vendor-doc` | Google-first 가입(비밀번호 미설정) 사용자는 Re-authentication 으로 재인증 수단 없어 lockout 가능 — 사용자 population 조사 필요(`needs-confirmation`) |
|
|
| D3 | Account Console self-service unlink lockout 방지는 **Keycloak 엔진이 서버에서 이미 강제** (마지막 federated identity 제거는 `count>1 \|\| user.isFederated() \|\| isPasswordSet()` 아니면 HTTP 400). "사전 password 설정 강제" 는 보안 필수 아니라 UX 개선으로 재분류 — **본 branch owned** (spa-ux D4 위임) | password 미설정 + 단일 federated identity → 엔진이 unlink 자동 거부. project 는 400 을 UX 로 처리(에러 안내 or 선제 "password 먼저 설정")만. 대안(프로젝트 자체 lockout 가드 구현) = 불필요(중복) | `raw/official-docs/keycloak-account-console-unlink-lockout-guard-official.md#KC-UNLINKGUARD-C1` (guard 조건 — 마지막 federated identity 제거 HTTP 400), `#KC-UNLINKGUARD-C2` (`isPasswordSet()` 구현), `#KC-UNLINKGUARD-C3` ("You can not remove last federated identity as you do not have a password.") | `official-vendor-doc` (엔진 소스코드 직접 근거) | keycloak `main` branch 기준(2026-07-15) — 배포 release tag 별 재확인 필요. narrative Admin Guide 미기재(소스코드가 유일 근거) |
|
|
| D4 | Self-service link (Client Initiated Account Linking / `idp_link`) 시 기존 password 재확인 강제 — **공식 근거 없음 + 반대 근거 존재**. self-service link 는 role(`account.manage-account-links`) + 유효 access token 만 요구, password 재확인 미언급 | N/A (근거 부재로 보류). project 가 step-up 을 원하면 `idp_link` AIA 앞에 커스텀 재인증 삽입 필요 | `UNSUPPORTED_DECISION` — 반대 근거 `raw/official-docs/keycloak-client-initiated-account-linking.md#KC-CIAL-C2` (link 전제 = role + scope 만) | — | `idp_link` kc_action(AIA) 재인증 강제 여부 미조사(`needs-confirmation`) — 별도 branch |
|
|
| D5 | Sync Mode 선택은 takeover 정책과 직교하며 이 branch가 값을 소유하지 않음 | D1의 persistent link key는 Sync Mode로 바뀌지 않는다. 값·선택 조건은 owner에서만 변경 | [[raw/branch-notes/feature-keycloak-google-claim-attribute-mapping]] D3 — attribute synchronization policy owner | `delegated` | 원 결정문 "Scenario B 회피"는 인과 오류. 본 branch는 link key 불변식만 소유하고, email 값 충돌의 실제 동작은 `needs-confirmation` |
|
|
|
|
## 구현 가이드
|
|
|
|
> *결정(Decisions)* 이 "*무엇*" 이라면 본 §는 "*어디에 어떻게*" 의 사전 명세. 본 브랜치는 `documented-only` 학습 노트이므로 구현 detail 은 **공식 문서가 규정하는 프로토콜·값 / 엔진 소스**를 anchor 로 하고 코드 미확인 항목은 `planned`/`UNSUPPORTED_IMPL_DECISION` 로 명시한다.
|
|
> 본 branch 의 in-scope owned 구현 대상은 **D1(sub key 정책)** 과 **D3(unlink lockout — 엔진 확인)** 뿐. [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D2 — flow 구성 owner. [[raw/branch-notes/feature-keycloak-google-claim-attribute-mapping]] D3 — Sync Mode owner. 본 branch는 두 결정을 재진술하지 않는다.
|
|
|
|
### 1. sub 기반 federated identity (link key) — D1
|
|
|
|
> **Trace**: D1 + `GOIDC-C3` + `KC-FLF-C2`.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: (a) `FEDERATED_IDENTITY` 테이블의 composite key/컬럼명(`FEDERATED_USER_ID` 에 sub 저장)은 본 Sources 미문서화 → Claims To Verify, `planned`. trade-off: 학습 단계엔 planned, 실 구현 시 DB/admin guide 확인. (b) OOTB `Create User If Unique` 는 **email/username** 으로 충돌 감지(`KC-FBLVERIFY-C5`) → sub-only 매칭엔 커스텀 authenticator 필요. 이 gap 은 본 branch 범위 밖(별도 branch, §Audit).
|
|
|
|
| 항목 | 값 / 명세 | 근거 |
|
|
|---|---|---|
|
|
| link key | Google `sub` → federated identity `FEDERATED_USER_ID` (Keycloak built-in) | D1, `GOIDC-C3` |
|
|
| email 역할 | user attribute 만 (link key 아님). Attribute Importer 매핑 owner = [[raw/branch-notes/feature-keycloak-google-claim-attribute-mapping]] D4 | D1 |
|
|
| auto-link 금지 lever | First Broker Login "Automatically Link Existing Account"/AutoLink `DISABLED` (owner = [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D1) | delegate, `KC-FLF-C2`, `KC-FBLVERIFY-C4` |
|
|
| 충돌 감지 방식 gap | OOTB 는 email/username 매칭 → sub-only 원하면 커스텀 authenticator (별도 branch) | `KC-FBLVERIFY-C5`, §Audit |
|
|
|
|
### 2. 기존 계정 link 시 재인증 관철 — D2 (정책 수준; flow 구성 delegate)
|
|
|
|
> **Trace**: D2 + `KC-FBLVERIFY-C1`/`C2`/`C3`.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: 정확한 authenticator step 값(`REQUIRED`/`ALTERNATIVE`/`DISABLED`)의 flow *구성* owner = [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D2. 본 branch 는 *정책*(secret 증명 필요)만 소유. trade-off: flow 편집 결정은 sibling.
|
|
|
|
| 항목 | 정책 / 명세 | 근거 |
|
|
|---|---|---|
|
|
| 기본 동작(주의) | SMTP 설정 realm → "Verify Existing Account By Email" 이 기본(`ALTERNATIVE`) 실행(secret 미증명) | `KC-FBLVERIFY-C1` |
|
|
| password 재인증 관철 | admin 이 "Verify Existing Account By Email" DISABLE → "Verify Existing Account By Re-authentication" 실행 | `KC-FBLVERIFY-C2` / `KC-FBLVERIFY-C3` |
|
|
| lockout 주의 | 비밀번호 미설정(Google-first) 사용자는 Re-authentication 불가 → 다른 연결 IdP 재인증 경로 or fallback 설계 필요 | `KC-FBLVERIFY-C3` |
|
|
|
|
### 3. self-service unlink lockout — D3 (엔진 강제; project 는 UX 만)
|
|
|
|
> **Trace**: D3 + `KC-UNLINKGUARD-C1`/`C2`/`C3`.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: 클라이언트 UX 처리 방식(에러 표시 vs 선제 안내)은 본 Sources 미규정 — project 선택. trade-off: 학습 단계엔 기본 Account Console 400 메시지 노출(`planned`).
|
|
|
|
| 항목 | 동작 | 근거 |
|
|
|---|---|---|
|
|
| 엔진 가드 | 마지막 federated identity 제거 시 `count>1 \|\| user.isFederated() \|\| isPasswordSet()` 아니면 HTTP 400 | `KC-UNLINKGUARD-C1`, `KC-UNLINKGUARD-C2` |
|
|
| 사용자 메시지 | "You can not remove last federated identity as you do not have a password." | `KC-UNLINKGUARD-C3` |
|
|
| project 작업 | 400 을 UX 로 처리(에러 안내 or 선제 "password 먼저 설정") — 보안 아닌 UX | D3 |
|
|
| unlink 후 재로그인 | Federated Identity record 삭제 → 재로그인 시 First Broker Login 재실행 → §2 재인증 경로 | delegate [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D2 |
|
|
|
|
## 엣지·실패·의존
|
|
|
|
> R4 캡처용. 정상 경로 외에 *구현 중 부딪힐* 실패/엣지/다른 계약 의존을 미리 열거.
|
|
|
|
- **실패·엣지 경로**:
|
|
- **Scenario A (email 재배정 takeover, §Takeover)**: 방어 = D1(sub key). `sub_B` ≠ `sub_A` → 신규 user. email 기반이면 탈취.
|
|
- **Scenario B (self email 변경, §Takeover)**: 방어 = D1(sub key) — 이미 링크된 `sub_E` 는 재로그인 시 email 재매칭 없이 자기 계정으로 라우팅. **Sync Mode 무관**(§Audit 정정). FORCE 면 email attribute 값만 갱신 → takeover 아닌 **email 값 충돌** 리스크(realm "Duplicate emails" 설정 의존, `needs-confirmation`).
|
|
- **Scenario C (email_verified=false, §Takeover)**: [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D4 — silent-link 방지 owner. [[raw/branch-notes/feature-keycloak-idp-brokering-google-client]] D6 — IdP email trust 설정 owner.
|
|
- **orphan account unlink (D3)**: password 미설정 Google-only 사용자가 마지막 link 제거 → 엔진이 서버에서 HTTP 400 거부(`KC-UNLINKGUARD-C1`). project 는 400 UX 처리만.
|
|
- **Google-first 가입 lockout (D2)**: 비밀번호 미설정 사용자가 두 번째 IdP 충돌 시 Re-authentication 재인증 수단 없어 막힐 수 있음(`needs-confirmation`).
|
|
- **OOTB email-collision 매칭 (D1 tension)**: OOTB `Create User If Unique` 는 email/username 매칭(`KC-FBLVERIFY-C5`) → sub-only 매칭엔 커스텀 authenticator 필요(별도 branch, §Audit).
|
|
- **다른 계약 의존**:
|
|
- [[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-first-broker-login-flow]] D4 — hard-reject SPI 없이 성립하는 linking core owner.
|
|
- [[raw/branch-notes/feature-keycloak-google-claim-attribute-mapping]] D3 — Sync Mode owner. [[raw/branch-notes/feature-keycloak-google-claim-attribute-mapping]] D4 — email attribute mapping owner.
|
|
- [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]] D4 — unlink UX 진입점 owner. [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]] D5 — client-initiated linking owner.
|
|
- [[raw/branch-notes/feature-keycloak-idp-brokering-google-client]] D6 — IdP email trust 설정 owner.
|
|
|
|
## 검증해야 할 주장
|
|
|
|
> 공식 문서 근거가 있어도 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| Keycloak `FEDERATED_IDENTITY` 테이블의 composite key 는 `(IDENTITY_PROVIDER, FEDERATED_USER_ID, USER_ID)` 이며 FEDERATED_USER_ID 에 Google `sub` 가 저장됨 | 본 branch 의 Sources 는 Keycloak 내부 스키마를 다루지 않음. 본문 메모 자체가 "추정" 표기 | Keycloak DB 직접 조회 또는 official server-installation guide 의 schema 섹션 확인 | `needs-confirmation` |
|
|
| OOTB `Create User If Unique` 가 email/username 이 아니라 `sub` 로 충돌 감지하게 하려면 커스텀 authenticator 가 필요하다 (D1 과 OOTB flow 의 구조적 긴장) | 조사에서 OOTB 는 "same email or username" 매칭 확인(`KC-FBLVERIFY-C5`) — sub-only 커스텀 구현 필요 여부는 미검증 | Keycloak `IdpCreateUserIfUniqueAuthenticator` 소스/SPI 문서 확인 + dev 환경에서 sub 충돌 재현 | `needs-confirmation` |
|
|
| Google 이 실제로 primary email 변경을 허용하며 그 결과 ID token 의 `email` claim 이 변경된다 (Scenario B 의 전제) | GOIDC-C3 는 "email 변경 가능성" 을 함의하지만 Google primary email 변경의 정확한 정책 (alias vs primary) 은 본 인용 범위 밖 | Google Account 공식 help 페이지 추가 수집 또는 실제 dev Google 계정으로 변경 시도 | `needs-confirmation` |
|
|
| `email_verified=false` 인 Google ID token 이 실제로 발급될 수 있는 시나리오가 존재 | 본 branch 의 Sources 는 `email_verified` semantics 의 정확한 조건을 직접 인용하지 않음 | Google OIDC `email_verified` claim 공식 spec 인용 추가 수집 | `needs-confirmation` |
|
|
| Google Workspace 에서 퇴사자 email 이 신규 직원에게 재배정 가능 (Scenario A 의 전제) | codemancers tech-blog 만 언급 — `company-case-study` 등급 → 공식 best practice 로 단정 금지 | Google Workspace Admin 공식 문서 (user delete + recreate 정책) 인용 추가 수집 | `needs-confirmation` |
|
|
| owner가 선택한 Sync Mode가 공식 semantics와 같은 attribute synchronization 결과를 내며 D1의 link key를 바꾸지 않는다 | 값 선택은 [[raw/branch-notes/feature-keycloak-google-claim-attribute-mapping]] D3 소관이고, 본 branch는 takeover 직교성만 검증 | dev realm에서 owner profile 적용 후 attribute 변경·재로그인 결과와 federated identity key 불변을 함께 확인 | `needs-confirmation` (source-grounded) |
|
|
| realm "Duplicate emails" 설정과 Sync Mode FORCE 의 상호작용 — FORCE 로 email 이 충돌 값으로 갱신될 때 실제 동작(성공/실패/무음 충돌) | 조사 범위 밖 — 공식 문서 미확인 영역 (Plan Gap) | Keycloak Admin Console > Realm Settings > "Duplicate emails" 값 확인 + dev realm 에서 email 충돌 재현 | `needs-confirmation` |
|
|
| 인용한 엔진 가드/authenticator 동작이 실제 배포 Keycloak **release tag** 에서도 동일하다 | 인용 소스(`LinkedAccountsResource.java`, `first-login-flow.adoc`)는 keycloak `main` branch(2026-07-15) 기준 | 배포 예정 버전 tag 로 소스/문서 재확인 | `needs-confirmation` |
|
|
| `idp_link` kc_action(Application Initiated Action) 이 재인증을 강제하는가 (D4 최종 답) | 조사에서 client-initiated linking 은 role+token 만 요구 확인(`KC-CIAL-C2`), `idp_link` AIA 는 미조사 | `IdpLinkAction` 소스 + Application Initiated Actions 공식 문서 조사 (별도 branch) | `needs-confirmation` |
|
|
|
|
## 감사와 발견 사항
|
|
|
|
> 2026-07-15 `/branch-spec` 자동조사(공식 문서 + 엔진 소스) 결과. 사용자 작성 결정은 verbatim 보존, 아래는 정합 권고·정정만 (CLAUDE.md §11).
|
|
|
|
- **RESOLVED — RESTATED_FOREIGN_DECISION**: 본 branch 의 core owned 결정은 **D1(sub key)** + **D3(unlink lockout safety — spa-ux D4 위임)** 뿐이다. **D2**는 [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] D2, **D5**는 [[raw/branch-notes/feature-keycloak-google-claim-attribute-mapping]] D3 를 Reference-Only로 consume하도록 정리했다.
|
|
- **RESOLVED — RATIONALE_CORRECTION (D5)**: 원 결정문 "Sync Mode = IMPORT → Scenario B 회피" 는 Historical/superseded로 격리했다. Active D5는 D1이 takeover를 차단하고 Sync Mode는 직교한다는 lens와 owner pointer만 유지한다.
|
|
- **CORRECTION (D2)**: 원 TODO/결정의 "Verify Existing Account by Re-authentication REQUIRED(기본)" 은 부정확 — SMTP 설정 realm 은 "Verify Existing Account By Email" 이 기본 `ALTERNATIVE`(`KC-FBLVERIFY-C1`). password 재인증을 관철하려면 admin 이 email authenticator 를 **명시적으로 DISABLE** 해야 한다(`KC-FBLVERIFY-C2`).
|
|
- **UPGRADE (D3, UNSUPPORTED → CONFIRMED)**: D3 는 기존 `UNSUPPORTED_DECISION` 이었으나 엔진 소스(`LinkedAccountsResource#removeLinkedAccount` 의 `count>1 || user.isFederated() || isPasswordSet()` 가드 + `federatedIdentityRemovingLastProviderMessage`)로 **CONFIRMED**(`KC-UNLINKGUARD-C1`~`C3`). lockout 은 Keycloak 서버 가드가 이미 방지 → project 작업은 UX 처리로 축소. **narrative Admin Guide 미기재** — 소스코드가 유일 근거.
|
|
- **CORRECTION (D4, UNSUPPORTED 유지 — 사유 격상)**: "self-service link 시 password 재확인" 은 근거 부재가 아니라 **확인된 반대 근거**(link 전제 = role + token 만, `KC-CIAL-C2`). project 가 이 정책을 원하면 커스텀 구현 필요.
|
|
- **OUT_OF_BRANCH_SCOPE (이관 권고)**: OOTB `Create User If Unique` 는 email/username 으로 충돌 감지(`KC-FBLVERIFY-C5`, sub 아님). D1(sub-only linking)과 OOTB flow 의 **구조적 긴장** — sub-only 매칭엔 커스텀 authenticator 필요. 본 branch 범위 밖 → **별도 branch 로 이관 권고**(예: `feature-keycloak-sub-based-collision-authenticator`). 본 §에 이관 history 보존, §구현 가이드 §1 에는 gap 표시만.
|
|
- **STALE_REVERSE_REF (Should-fix, `/sync` 대상)**: D3 가 `UNSUPPORTED_DECISION` → `official-vendor-doc` 로 승격됐으므로, 이를 참조하는 [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]] 의 D4/§구현 가이드/§엣지 (해당 노트에서 본 branch D3 를 `needs-confirmation`/`UNSUPPORTED` 로 요약한 참조들)이 stale. `/sync` 또는 `wiki-doc-author` mode=migrate 로 역참조 전파 필요(비차단).
|
|
|
|
## 마주친 문제
|
|
|
|
- 아직 없음(문서 단계).
|
|
|
|
## 묶음
|
|
|
|
<!-- GENERATED: sources:start -->
|
|
- [[raw/official-docs/google-openid-connect-oidc]]
|
|
- [[raw/official-docs/keycloak-account-console-unlink-lockout-guard-official]]
|
|
- [[raw/official-docs/keycloak-first-broker-login-flow]]
|
|
- [[raw/official-docs/keycloak-first-broker-login-verify-authenticators-official]]
|
|
- [[raw/official-docs/keycloak-first-login-flow]]
|
|
- [[raw/official-docs/keycloak-identity-provider-sync-mode-official]]
|
|
<!-- GENERATED: sources:end -->
|
|
|
|
> 본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
|
|
|
|
### 근거 자료
|
|
|
|
- [[raw/company-tech-blogs/keycloak-google-login-codemancers]]
|
|
- [[raw/official-docs/keycloak-first-login-flow]]
|
|
- [[raw/official-docs/keycloak-first-broker-login-verify-authenticators-official]] — D2 근거 (verify authenticators 기본 트리거 조건)
|
|
- [[raw/official-docs/google-openid-connect-oidc]]
|
|
- [[raw/official-docs/keycloak-identity-brokering-overview-official]]
|
|
- [[raw/official-docs/keycloak-account-console-unlink-lockout-guard-official]] — D3 근거 (self-service unlink lockout guard, 엔진 소스)
|
|
- [[raw/official-docs/keycloak-identity-provider-sync-mode-official]] — D5 근거 (Sync Mode 공식 의미론)
|
|
- [[raw/official-docs/keycloak-client-initiated-account-linking]] — D4 반대 근거
|
|
|
|
### 오류 기록 (이 sub-sub-branch 작업 중 발생)
|
|
|
|
- (없음 — 현재 documented-only 단계)
|
|
|
|
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
|
|
|
- (없음 — Phase 3 실 구현 단계에 누적)
|
|
|
|
## 관련 일일 노트
|
|
|
|
|
|
## 완료 후 정리
|
|
|
|
- PR 링크: (없음, 문서까지만)
|
|
- 머지 결과 / 배포 환경: 없음 (`documented-only`)
|
|
- **wiki 추출 대상:** 없음. P1B sub-sub-branch는 root 정책상 wiki/projects/ 승급 안 함.
|
|
- **추출하지 않을 항목:** 본 sub-sub-branch 전체 (`documented-only` / `planned`).
|