Files
llm-wiki/raw/branch-notes/feature-keycloak-three-leg-trust-chain.md
T

273 lines
31 KiB
Markdown

---
title: branch / feature-keycloak-three-leg-trust-chain (3-leg trust chain — Browser ↔ Keycloak ↔ Google 검증 메커니즘)
source_type: branch-note
status: raw
id: BR-KEYCLOAK-CHILD-11A28CFF
kind: branch-child
project: keycloak-patterns-overview
work_item: WI-KEYCLOAK-PATTERNS-OVERVIEW-015
inherits: [DEC-KEYCLOAK-PATTERNS-OVERVIEW-IDP-BROKERING-001@1]
refines: []
overrides: []
depends_on: [WI-KEYCLOAK-PATTERNS-OVERVIEW-003]
contract_packet: 1
branch: feature-keycloak-three-leg-trust-chain
parent_branch: feature-keycloak-idp-brokering-google-client
related_projects: [keycloak-patterns]
tags: [branch, keycloak-patterns, spa, idp-brokering, google-federation, trust-chain, jwt, p2b]
created: 2026-05-25
target_merge:
status_label: in-progress
contract_packet_sha256: 1af4709799789103babb4b3503ffefc67f35e51a4eb8c5175c4928267a419143
---
# branch: feature-keycloak-three-leg-trust-chain (3-leg trust chain 검증)
> Layer: `raw/branch-notes/` — [[raw/branch-notes/feature-keycloak-idp-brokering-google-client]]의 WI015 child branch.
> 학습 노트. P2B는 `documented-only` 단계.
> **본 노트의 역할 (2026-07-18 `/branch-spec` 정리)**: 본 노트는 부모 P2B([[raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation]]) 가 **hop 별 검증 매트릭스의 owner 로 위임한** 결정(D5)의 정본이다(부모 §신뢰 경계 delegation). 부모는 이 D5 를 consume 만 하며, 본 노트가 각 hop 의 *누가·무엇을·어떻게 검증하는가* 를 소유한다.
<!-- section-id: branch-parent -->
## 부모 (필수)
[[raw/branch-notes/feature-keycloak-idp-brokering-google-client]]
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: Google login·Keycloak user mapping 200과 application diff 0줄이 검증된다
<!-- 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을 사용한다 | Browser·Keycloak·Google의 hop별 trust verification에 적용한다 | [[raw/project-notes/keycloak-patterns-overview]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
없음.
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
P2A는 2-leg trust (Browser ↔ Keycloak)였으나, P2B는 **3-leg trust** (Browser ↔ Keycloak ↔ Google). 각 hop마다 **누가 무엇을 검증하는지** 명확히 정리.
면접 질문: "Google 로그인이 추가되면 신뢰 검증이 어떻게 늘어나나요?"
→ "OIDC 표준상 RP는 Google ID token의 signature·issuer·audience·expiry와 요청에 보낸 `nonce` 일치를 검증해야 합니다. 이 배치에서는 Keycloak이 RP 역할을 맡지만, target Keycloak 버전이 nonce를 자동 송신·대조하는 제품 동작은 아직 wire trace로 확인하지 않았습니다. 이후 Keycloak이 자체 서명한 access token을 발급하고 backend는 Keycloak issuer/JWKS만 신뢰합니다."
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- **Hop 1: Google → Keycloak** — Google ID token signature 검증
- **Hop 2: Keycloak → SPA** — Keycloak access token signature 발급
- **Hop 3: SPA → Backend** — backend의 Keycloak JWT 검증
- Issuer 검증 규칙:
- Google `iss=https://accounts.google.com`**정확히 이 문자열**. (⚠️ **정정 2026-07-18**: 원래 여기 "또는 `accounts.google.com` — spec 상 둘 다 허용" 이라 적었으나 **사실과 다르다**. 아카이브 근거 `raw/official-docs/google-oidc-discovery-spec.md#GOOGLE-OIDC-C1` 의 discovery JSON 은 `iss` 를 정확히 `https://accounts.google.com` 로만 규정하고, 그 "Does not prove" 열이 bare-hostname alias 를 명시적으로 부인한다. §Claims To Verify CV1 참조.)
- Keycloak `iss=https://kc.example.com/realms/{realm-name}`
- JWKS rotation 정책 비교 (Google vs Keycloak)
- Keycloak이 Google jwks_uri를 캐시하는 방식
### 제외 범위
- 코드 변경 없음 검증 — [[raw/branch-notes/feature-keycloak-federation-spa-zero-change]]
- claim mapping — [[raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role]]
- Account Linking — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]
- **발급 측 `aud` 주입 + 백엔드 audience 검증 구현** — [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1·D4 소유. 본 노트 Hop3-`aud` cell 은 그 계약을 consume 만.
- **`iss` 문자열 byte-match 를 성립시키는 `KC_HOSTNAME` 고정** — [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1 소유. 본 노트 Hop3-`iss` cell 의 전제.
## 근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/keycloak-first-broker-login-flow]] | First Broker Login Flow 존재 + account linking 시점 (D4) |
| [[raw/official-docs/spring-security-resource-server-jwt]] | 백엔드가 단일 issuer(Keycloak) JWKS 로 self-configure + `iss`/`aud` 검증 (D1, D5 Hop3, CV4) |
| [[raw/official-docs/google-oidc-discovery-spec]] | Google issuer 문자열 + RS256 + nonce Required + local validation (D5 Hop1, D3, CV1) |
| [[raw/official-docs/security-jwt-rfc-7519-validation]] | JWT `aud` MUST-reject / `exp` MUST-NOT-accept (D5 Hop1·Hop3 aud·exp) |
| [[raw/official-docs/google-oauth2-redirect-uri-validation-official]] | redirect URI 정확 매칭 → `redirect_uri_mismatch` (CV7) |
| [[raw/official-docs/openid-connect-core-id-token-validation]] | OIDC Core 1.0 §2/§3.1.2.1/§3.1.3.7 — ID Token `aud`=RP client_id + `nonce` 검증 mandatory (`OIDC-CORE-C1`~`C5`; D5 Hop1-aud·nonce, D3) |
| [[raw/official-docs/jwks-keycloak-key-rotation-active-passive]] | Keycloak 이 자체 active key pair 로 새 서명 생성 (`KC-ROT-C1`) — **D5 Hop2(Keycloak→SPA 재서명) 근거로만**. ⚠️ 아래 disambiguation |
> **인용 범위 한정 (disambiguation)**: `raw/official-docs/jwks-keycloak-key-rotation-active-passive.md` (KC-ROT-C*) 는 **Keycloak 자신의 realm 서명키** active-passive 회전이다 — D5 **Hop2**(Keycloak 이 자체 키로 재서명)의 근거로는 유효하나, **외부 Google JWKS 캐시**(D2)와는 무관하다. D2 근거로 인용하면 파일명의 "JWKS" 에 낚인 `FILENAME_INFERENCE` 오용이다.
## TODO
- [x] 3-leg 다이어그램 작성 (각 hop의 검증 항목 표기) — 등급: `documented-only` (§구현 가이드 §1 매트릭스로 승격)
- [x] Google ID token 검증 항목 정리: signature(RS256, JWKS), `iss`, `aud`, `exp`, `nonce` — 등급: `documented-only` (§구현 가이드 §1, cell 별 근거)
- [x] Google `iss` 허용값 — **정정 완료**: `https://accounts.google.com` **단일** (bare-hostname alias 는 spec 부인). 근거 `#GOOGLE-OIDC-C1` — 등급: `documented-only` (CV1 resolved-as-contradiction)
- [x] Keycloak access token 검증 항목 정리 (backend 측): signature, `iss`, `aud`, `exp` — 등급: `documented-only` (§구현 가이드 §1 Hop3, `SSRS-JWT-C1/C2/C6`). ※`azp`/`typ=Bearer` 는 본 노트 Sources 에 직접 인용 없음 → 형제 audience-validator 매트릭스 소관
- [ ] Google JWKS rotation 빈도 (수일~수주 주기, 정확한 SLA 없음) — 등급: `needs-confirmation` (CV2, 아카이브 부재 — live header 관찰 필요)
- [ ] Keycloak이 Google jwks_uri를 캐시하는 정책: 기본 캐시 TTL, expired key fallback — 등급: `needs-confirmation` (CV3/D2, 아카이브 부재 — Keycloak IdP config doc/소스 필요)
- [ ] **함정**: Keycloak이 Google JWKS 캐시 갱신 실패 시 Google 로그인 전체 장애 → fallback 정책 확인 — 등급: `needs-confirmation` (§엣지·실패·의존)
- [x] **함정**: backend가 Keycloak issuer를 잘못 적으면 모든 Google-originated token 거부 — 등급: `documented-only` (CV4 resolved, `SSRS-JWT-C6` — 단 local 재현은 `planned`)
## 진행 중 메모
- 핵심 통찰: **backend는 Google을 모른다.** backend의 JWT validation 코드는 Keycloak issuer / Keycloak JWKS만 본다. Google 추가/제거는 backend 코드에 영향 없음.
- **누가 무엇을 검증하나** 정리 (§구현 가이드 §1 로 명세 승격 — 아래는 원본 통찰 보존):
| Hop | 검증자 | 검증 대상 | 검증 항목 |
|-----|--------|-----------|----------|
| Google → Keycloak | Keycloak | Google ID token | signature(RS256, Google JWKS), `iss`, `aud=Keycloak이 보유한 Google client_id`, `exp`, `nonce` |
| (Keycloak 내부) | Keycloak | First Broker Login Flow 정책 | `email_verified`, `hd`, Account Linking 결정 |
| Keycloak → SPA | (Keycloak이 발급) | Keycloak access token | (Keycloak이 RS256 서명) |
| SPA → Backend | Backend | Keycloak access token | signature(Keycloak JWKS), `iss=https://kc/realms/{r}`, `aud=<backend-client-id>`, `exp` |
- Google JWKS 키 갱신 빈도가 빠른 편. spec에는 정해진 SLA 없음. Keycloak이 캐시한 키가 만료되었을 때 자동 refetch 필요.
- **redirect URI 변조 방지**: Google Cloud Console에 등록된 redirect URI 외 거부. Keycloak broker endpoint URL 변경 시 Google에도 반영 필요.
- **`nonce` 검증 계약**: OIDC RP는 요청의 nonce와 ID token nonce를 대조해야 한다. Keycloak이 이를 자동 송신·대조하는 target-version 제품 동작은 `needs-confirmation`이며 HAR/source 확인 전 사실형으로 표현하지 않는다.
- **2026-07-18 (`/branch-spec` 채움 pass)** — pre-template(2026-05-25) 노트를 템플릿 정합으로 보강: `선택 조건`(R2) 열 추가 · `## 구현 가이드`(§1 Hop별 검증 계약 · §2 issuer/audience 문자열 정합) · `## 엣지·실패·의존` 추가 · Sources 1→6 확장 · 섹션 순서 템플릿 정렬(Cluster 하단·Sources 상단). **NO_GROUND_TRUTH** — keycloak-patterns 는 실 구현 repo(`/home/donghyeon/workspace/keycloak-patterns/`, 부재 확인)가 없어 전 항목 `documented-only`/`needs-confirmation`(코드 grep 불가). ca-tmpl 은 별개 프로젝트지만 `APP_SECURITY_JWT_ISSUER` env-key + `AUTH_ISSUER_MISMATCH` 에러코드가 D1/CV4 의 "단일 issuer" 원리를 실물로 예시(cross-project 참고, 본 노트 등급엔 미반영).
- **자동조사** — `wiki-research-lane` 로 기존 아카이브 8+5 문서를 재앵커링: D1 + D5 의 12 cell 중 8개가 official 근거 획득(이전엔 D4 외 전부 `UNSUPPORTED_DECISION`). 신규 조사 1건(OIDC Core spec 아카이브)으로 Hop1-`aud`/`nonce` cell 을 승격. deferred 3건(Keycloak 외부 JWKS 캐시·Google rotation cadence·Keycloak 기본 서명 알고리즘 — 아카이브 문서로 안 닫히는 runtime/config-empirical 항목, §Claims To Verify 로 위임).
## 결정 사항 (decisions)
- 2026-05-25: **backend는 Keycloak JWKS만 신뢰** — Google JWKS는 backend 측에서 절대 검증하지 않음. 이유: 신뢰 경계 단순화. backend 입장에서 IdP는 Keycloak 하나.
- 2026-05-25: **Google JWKS rotation은 Keycloak 책임** — Keycloak realm export / restore 시 캐시 초기화될 수 있음. 운영 시 모니터링 항목.
- 2026-05-25 (정합 2026-07-18): **`nonce` 검증은 OIDC RP 의무**다. Keycloak 자동 처리 및 비활성화 옵션 존재 여부는 target version에서 미검증이다.
## 결정-근거 매핑
> 본 branch 의 결정-근거 매핑. `선택 조건` 열(R2, 2026-07-18 `/branch-spec` 추가): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없는 원리 진술은 `N/A`.
> **2026-07-18 재앵커링**: 이전 판은 D4 외 전부 `UNSUPPORTED_DECISION` 이었다(당시 Source 가 `keycloak-first-broker-login-flow` 1개뿐). `wiki-research-lane` 가 기존 아카이브에서 D1·D5(8/12 cell)·D3(일부)의 직접 근거를 발굴 + OIDC Core 신규 아카이브로 Hop1-aud/nonce 를 닫아 재mapping 했다. D2 와 D5 의 4 cell 은 genuine gap 으로 남아 라벨 유지.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | backend 는 Keycloak JWKS 만 신뢰 (Google JWKS 는 backend 측에서 절대 검증하지 않음) | 백엔드가 JWT 를 **직접 검증**하는 Resource Server(P2B)면 단일 issuer(Keycloak)만 신뢰가 기본 — `issuer-uri` 한 줄이 정확히 1개 AS 로 self-configure. **대안**(백엔드가 Google JWKS 도 검증)은 신뢰 경계를 2개로 늘려 federation 추가/제거마다 백엔드 변경 유발 → 기각. 백엔드가 JWT 자체를 안 보는 구성이 필요하면 배치를 Edge forward-auth(P1B)로 바꿈(별도 패턴) | `raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C1` (issuer-uri 로 self-configure + iss 검증), `raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C2` (4단계 deterministic discovery — 단일 AS 의 jwks_url) | `official-vendor-doc` | multi-issuer 는 `JwtIssuerAuthenticationManagerResolver` 별도(범위 밖). "신뢰 경계 단순화" 는 이제 mechanism 근거 보유(이전 UNSUPPORTED 해소). cross-project 실물 예시: ca-tmpl `APP_SECURITY_JWT_ISSUER`(단일 issuer URI 필수) + `AUTH_ISSUER_MISMATCH` 에러코드 |
| D2 | Google JWKS rotation(=외부 IdP JWKS 신선도)은 Keycloak 책임 (realm export/restore 시 캐시 초기화 가능, 운영 모니터링 항목) | Keycloak 이 broker 로서 Google id_token 을 검증하는 모든 P2B 구성 — **대안 없음**: 백엔드가 Google 을 안 보므로(D1 의 따름) Google JWKS 신선도는 구조상 Keycloak 만 담당 가능. 단 캐시 TTL/fallback *동작*은 미검증 | UNSUPPORTED_DECISION (Keycloak 의 외부 IdP JWKS 캐시 정책/TTL/fallback 은 아카이브 부재 — `wiki-research-lane``keycloak-identity-brokering-overview-official`·`keycloak-identity-broker-spi`·`keycloak-import-export-realms` 3개 추가 확인했으나 어느 것도 미기술. **`jwks-keycloak-key-rotation-active-passive.md` 는 Keycloak 자체 realm 키 회전이라 D2 근거 아님** — 위 disambiguation) | UNSUPPORTED_DECISION | 캐시 refetch 실패 시 Google 로그인 전체 5xx(§엣지·실패·의존). monitoring alarm 설계 전 Keycloak IdP config 페이지 또는 소스(`OIDCIdentityProvider`) 확인 의무 — §Claims To Verify CV3 |
| D3 | OIDC RP는 nonce를 송신하고 ID Token의 nonce 일치를 검증해야 한다. target Keycloak의 자동 처리 여부는 별도 제품 검증 | Google brokering의 replay 방지 계약. 표준 의무와 특정 제품 동작을 분리한다 | `raw/official-docs/google-oidc-discovery-spec.md#GOOGLE-OIDC-C5`, `raw/official-docs/openid-connect-core-id-token-validation.md#OIDC-CORE-C2`, `#OIDC-CORE-C5` | `official-standard` (의무) / `needs-confirmation` (Keycloak 제품 동작) | Keycloak 자동 송신·대조와 설정 토글은 HAR/source 확인 전 외부 주장 금지 |
| D4 | Keycloak 의 First Broker Login Flow 가 외부 IdP (Google) 로 첫 로그인 시 account linking 정책을 실행한다는 일반 진술 | N/A (원리 진술 — account linking 의 정확한 정책은 owner 형제 [[raw/branch-notes/feature-keycloak-first-broker-login-flow]] 소관) | `raw/official-docs/keycloak-first-broker-login-flow.md#KC-FBL-C1` (First login flow 존재 + sub-section 구조) | `official-vendor-doc` | KC-FBL-C2~C4 는 `needs-confirmation` — verbatim 재검증 보류. account linking 의 정확한 동작(자동 link vs prompt)은 owner 형제 소관 |
| D5 | Hop 별 검증 매트릭스 (Hop1 Google→Keycloak / Hop2 Keycloak→SPA / Hop3 SPA→Backend) — **부모 P2B 가 위임한 owner 결정** | N/A (검증 계약 명세, 분기 아님). 단 각 hop 검증 *주체*는 배치 의존: P2B(SPA direct)면 Hop3 검증자=백엔드 Resource Server, P1B(edge proxy)면 oauth2-proxy 대행(형제 패턴) | **cell 별**(§구현 가이드 §1 표): Hop1-sig `#GOOGLE-OIDC-C3`·`#GOOGLE-OIDC-C8`; Hop1-iss `#GOOGLE-OIDC-C1`; Hop1-aud `security-jwt-rfc-7519-validation.md#JWT-RFC7519-C1`(generic) + `openid-connect-core-id-token-validation.md#OIDC-CORE-C1`/`#OIDC-CORE-C4`; Hop1-exp `#JWT-RFC7519-C2`; Hop1-nonce `#GOOGLE-OIDC-C5`+`OIDC-CORE-C2`/`#OIDC-CORE-C5`; Hop2 `jwks-keycloak-key-rotation-active-passive.md#KC-ROT-C1`(자체 active key 서명); Hop3-sig `spring-security-resource-server-jwt.md#SSRS-JWT-C1`·`#SSRS-JWT-C2`; Hop3-iss `#SSRS-JWT-C1`·`#SSRS-JWT-C6`; Hop3-aud `#SSRS-JWT-C6`; Hop3-exp `#JWT-RFC7519-C2` | `official-standard + official-vendor-doc` (8/12 cell) | **4 cell 미해소**(§구현 가이드 §1 UNSUPPORTED_IMPL_DECISION): ① Hop1-aud 의 *Google-specific* "aud=Keycloak client_id"(generic RFC + OIDC Core 로 원리는 닫히나 Google 명시 quote 부재), ② Hop2 *default 알고리즘 RS256*(KC-ROT-C1 은 "자체 active key 서명"만 증명, RS256 명시 없음), ③ Keycloak-side nonce 자동 동작(D3 와 동일 gap). §Claims To Verify |
## 구현 가이드
> 본 branch 는 `documented-only` 학습 노트 — 실 구현 코드가 아니라 **각 hop 의 검증 계약**(누가·무엇을·어떻게 검증하는가)의 사전 명세다. 부모 P2B 가 이 §의 매트릭스를 hop 검증 owner 로 위임했다. 아래 sub-section 은 본 branch 결정(D1·D3·D5)에서만 도출한다.
>
> **3-rule**: R1 각 cell 은 Decision ID + Claim ID reference. R2 근거 없는 detail 은 `UNSUPPORTED_IMPL_DECISION` 라벨 + trade-off. R3 본 branch 범위 밖(발급측 aud 주입·KC_HOSTNAME 고정)은 owner 형제로 위임(§엣지·실패·의존 다른 계약 의존).
### 1. Hop별 검증 계약 (verifier → 대상 → 항목 → 근거)
> **Trace**: D5(전 cell) + D1(Hop3 단일 issuer 신뢰). 각 cell 은 아래 표의 Claim ID 로 근거. §진행 중 메모의 "누가 무엇을 검증하나" 통찰을 명세로 승격.
>
> - **UNSUPPORTED_IMPL_DECISION**:
> - **Hop1-aud (Google-specific)**: "Google id_token 의 `aud` = Keycloak 이 등록한 Google client_id" 의 *Google 명시* verbatim 은 아카이브에 없음. generic 근거(`JWT-RFC7519-C1` aud MUST-reject) + OIDC Core(ID Token aud=RP client_id)로 *원리*는 닫히나, "Google 이 그렇게 발급한다" 는 `INFERENCE`. trade-off: OIDC RP-client 표준 의미상 거의 확실하나 FACT 승격은 Google Identity 페이지 또는 실 토큰 decode 필요(§Claims To Verify).
> - **Hop2 서명 알고리즘 (RS256)**: `KC-ROT-C1` 은 "Keycloak 이 자체 active key pair 로 새 서명 생성"만 증명 — **default 알고리즘이 RS256 이라는 근거는 아카이브 부재**. trade-off: Keycloak 관례상 RS256 이 default 로 알려져 있으나 미검증 → 이 cell `needs-confirmation`.
> - **Hop1-nonce (Keycloak-side)**: nonce 의 필요/검증 *원리*는 spec(§2 근거), 그러나 Keycloak 이 *자동으로* 송신/대조하는지는 미아카이브(D3 Open Risk 와 동일).
| Hop | 검증자 | 대상 | 검증 항목 | 근거 (Claim ID) | cell 등급 |
|---|---|---|---|---|---|
| Hop1 Google→Keycloak | Keycloak | Google id_token | signature RS256 (Google JWKS, local 검증) | `#GOOGLE-OIDC-C3`(RS256-only), `#GOOGLE-OIDC-C8`(retrieve keys + validate locally) | `documented-only` |
| Hop1 | Keycloak | Google id_token | `iss` = `https://accounts.google.com` (정확 문자열) | `#GOOGLE-OIDC-C1` | `documented-only` |
| Hop1 | Keycloak | Google id_token | `aud` = Keycloak 의 Google client_id | `#JWT-RFC7519-C1`(generic aud MUST-reject) + `OIDC-CORE-C1`/`#OIDC-CORE-C4`(ID Token aud=RP client_id) | ⚠️ `UNSUPPORTED_IMPL_DECISION` (Google-specific quote 부재) |
| Hop1 | Keycloak | Google id_token | `exp` (만료 검증, clock-skew leeway) | `#JWT-RFC7519-C2` | `documented-only` |
| Hop1 | Keycloak | Google id_token | `nonce` 일치 (replay 방지) | `#GOOGLE-OIDC-C5`(nonce Required) + `OIDC-CORE-C2`/`#OIDC-CORE-C5` | ⚠️ `UNSUPPORTED_IMPL_DECISION` (Keycloak 자동 동작 미검증) |
| Hop2 Keycloak→SPA | (Keycloak 발급) | Keycloak access_token | 자체 active key 로 재서명 | `#KC-ROT-C1` (single active key pair → new signatures) | ⚠️ `needs-confirmation` (알고리즘 RS256 명시 부재) |
| Hop3 SPA→Backend | Backend RS | Keycloak access_token | signature (Keycloak JWKS, discovery) | `#SSRS-JWT-C1`, `#SSRS-JWT-C2` | `documented-only` |
| Hop3 | Backend RS | Keycloak access_token | `iss` = `https://kc/realms/{r}` (byte-match) | `#SSRS-JWT-C1`, `#SSRS-JWT-C6`("iss 가 아니면 validation fail") | `documented-only` |
| Hop3 | Backend RS | Keycloak access_token | `aud` = backend-client-id | `#SSRS-JWT-C6` (audiences property → aud 검증) | `documented-only` (발급측 aud 주입은 형제 audience-validator D4 의존) |
| Hop3 | Backend RS | Keycloak access_token | `exp` | `#JWT-RFC7519-C2` | `documented-only` |
### 2. audience 문자열 정합 규칙 (byte-match + 발급측 선행)
> **Trace**: D1 + D5 Hop3-iss(`#SSRS-JWT-C1`·`#SSRS-JWT-C6`) + Hop1-iss(`#GOOGLE-OIDC-C1`).
>
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 아래는 인용 claim 의 직접 도출.
| 항목 | 규칙 | 근거 | 의존(owner 형제) |
|---|---|---|---|
| Google `iss` | 정확히 `https://accounts.google.com` — bare-hostname alias 금지(비교 실패) | `#GOOGLE-OIDC-C1` (+ Does-not-prove: alias 부인) | — (CV1 correction) |
| Keycloak `iss` (Hop3) | 백엔드 `issuer-uri` == token `iss` **byte-level** 일치. discovery 로 self-configure, 불일치 시 validation fail | `#SSRS-JWT-C1`, `#SSRS-JWT-C6` | [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1 (`KC_HOSTNAME` 고정으로 iss 안정화) |
| Keycloak `aud` (Hop3) | `aud` 에 backend-client-id 포함해야 통과. 발급측이 안 넣으면 `aud=account` 만 → 정상 토큰도 거부 | `#SSRS-JWT-C6` | [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D4 (SPA client Audience mapper 선행) |
| Google redirect URI | Keycloak broker endpoint 를 Google Console 에 정확 등록 — 불일치 시 `redirect_uri_mismatch` | `raw/official-docs/google-oauth2-redirect-uri-validation-official.md#GOOGLE-REDIR-C3` | [[raw/branch-notes/feature-keycloak-google-redirect-uri-policy]] D1~D4 (exact-match 규칙 deep owner) + [[raw/branch-notes/feature-keycloak-idp-brokering-google-client]] D3 (등록 step) |
## 엣지·실패·의존
> R4 캡처용. 본 branch 는 `documented-only` 이나, 3-leg trust 를 실제로 세울 때 부딪힐 실패/엣지와 다른 계약 의존을 미리 열거.
- **실패·엣지 경로**:
- **Keycloak → Google JWKS refetch 실패 → Google 로그인 전체 5xx (가장 위험, 운영)**: Keycloak 이 Google `jwks_uri` 를 못 가져오면(네트워크 단절 / Google 측 변경) Hop1 signature 검증 불가 → 모든 Google 로그인 실패. 캐시 TTL·expired-key fallback 동작 **미검증**(D2/CV3 `UNSUPPORTED`). 기대 동작: 만료 시 자동 refetch(가정), monitoring alarm 필수.
- **백엔드 issuer 오설정 → 모든 Google-originated token 거부**: 백엔드 `issuer-uri` 가 Keycloak realm URL 과 byte-level 불일치면 Hop3 에서 `iss` 검증 실패 → Google 로 로그인한 사용자 포함 **전 토큰 401**. 근거 `#SSRS-JWT-C6`("iss 가 아니면 validation will fail"). 기대 동작: 의도적 오설정 시 401 (CV4, local 재현 `planned`).
- **Google `iss` alias 혼동 → Hop1 검증 실패**: `accounts.google.com`(bare)로 비교하면 `https://accounts.google.com` 발급 토큰이 불일치. 근거 `#GOOGLE-OIDC-C1`(정확 문자열). CV1 정정 사항 — 과거 "둘 다 허용" 은 folklore.
- **nonce 미검증/재사용 → replay 취약**: Hop1 에서 nonce 대조를 안 하면 탈취된 id_token 재생 가능(D3). 단 Keycloak 자동 검증 여부 미검증.
- **redirect URI 변조 → Google 거부**: Keycloak broker endpoint 외 URI 는 `redirect_uri_mismatch`(`#GOOGLE-REDIR-C3`). Keycloak broker URL 변경 시 Google Console 반영 필요.
- **Keycloak 서명 알고리즘 가정 오류**: 백엔드가 RS256 을 가정하는데 Keycloak realm 이 다른 알고리즘이면 Hop3 signature 검증 실패. Hop2 알고리즘 default 는 `needs-confirmation`(§구현 가이드 §1).
- **다른 계약 의존**:
- [[raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation]] (부모 P2B) — 부모가 hop 검증 매트릭스 owner 를 **본 노트 D5 로 위임**(부모 §신뢰 경계 delegation). 본 노트 D5 가 바뀌면 부모 §신뢰 경계 개요 갱신 필요(비차단 전파).
- [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] **D1**(백엔드 `iss`+sig+`exp`+`aud` 4종 검증) · **D4**(Keycloak Audience mapper 로 backend client_id 를 `aud` 에 주입) — 본 노트 **Hop3-aud cell** 이 그 계약을 consume. federation 환경에서도 `aud` 가 포함되는지는 §Claims To Verify CV5 로 그쪽에 위임.
- [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] **D1**(`KC_HOSTNAME` 고정 → token `iss` 가 백엔드 `issuer-uri` 와 byte-match) — 본 노트 **Hop3-iss cell + D1** 성립의 전제. iss 불일치 함정의 재현·해결은 그 branch 소관.
- [[raw/branch-notes/feature-keycloak-google-redirect-uri-policy]] **D1~D4**(redirect URI exact-match / byte-level 규칙 deep owner, GOOGLE-REDIR 근거 계열) + [[raw/branch-notes/feature-keycloak-idp-brokering-google-client]] **D3**(Authorized redirect URI 등록 step) — 본 노트 Hop1 redirect URI 변조 방지(CV7, `#GOOGLE-REDIR-C3`)가 이 계약에 의존.
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| **[CV1 — resolved-as-contradiction]** Google `iss``https://accounts.google.com``accounts.google.com` 둘 다 spec 상 정당한가 | 원래 "둘 다 허용" 으로 적었으나 아카이브가 **반증** | ✅ **해소**: `raw/official-docs/google-oidc-discovery-spec.md#GOOGLE-OIDC-C1` — discovery JSON 은 `iss` 를 정확히 `https://accounts.google.com` 로만 규정, Does-not-prove 열이 alias 를 명시 부인. 본문 §마주친 문제의 "과거 사례" 는 미검증 folklore | `resolved` (documented-only) |
| **[CV4 — resolved]** backend 가 Keycloak issuer 를 잘못 적으면 모든 Google-originated token 거부 | 메커니즘은 문서화, local 재현은 미실행 | ✅ 메커니즘 `raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C6`("iss 가 아니면 validation will fail"). local 재현(`issuer-uri` 의도적 오설정 → 401)은 별도 `planned` | `documented-only` (재현 `planned`) |
| **[CV7 — resolved]** Google Cloud Console redirect URI 정확 매칭(변조 방지) | Source 미등록이었음 | ✅ `raw/official-docs/google-oauth2-redirect-uri-validation-official.md#GOOGLE-REDIR-C3`(정확 매칭, else `redirect_uri_mismatch`). Sources 에 등록 완료 | `documented-only` |
| **[CV2 — gap]** Google JWKS rotation 빈도 + Cache-Control / SLA | 아카이브 부재 — `google-openid-connect-oidc`·`google-oidc-discovery-spec` 어디에도 cadence/헤더 없음 | `https://www.googleapis.com/oauth2/v3/certs` 응답 헤더(`Cache-Control: max-age=...`) 직접 관찰 또는 Google 지원 페이지 아카이브 | `needs-confirmation` (research opt-in) |
| **[CV3/D2 — gap]** Keycloak 의 외부 IdP(Google) JWKS 캐시 정책 (기본 TTL, expired-key fallback) | 아카이브 부재 — Keycloak IdP 문서 3개 확인했으나 미기술. `KC-ROT` 는 자체 키라 무관 | Keycloak Identity Provider admin config 페이지 또는 소스(`OIDCIdentityProvider`/`AbstractOAuth2IdentityProvider`) 정독 후 `raw/official-docs/` 등록 | `needs-confirmation` (research opt-in) |
| **[CV5 — delegated]** Keycloak 발급 access_token 에 `aud=<backend-client-id>` 포함(federation 환경에서도) | 본 노트 Sources 범위 밖 — 발급측 결정 | [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D4 + 실 토큰 decode. 본 노트는 Hop3-aud cell 로 consume | `needs-confirmation` (형제 위임) |
| **[CV6/D3 — gap]** Keycloak 이 Google 요청 시 random `nonce` 동봉 + 응답 id_token nonce 자동 대조 | spec 은 "RP 가 해야 한다"만 증명, Keycloak 자동 동작은 미아카이브 | Keycloak OIDC Identity Provider config doc(nonce/PKCE 토글) 또는 소스 `OIDCIdentityProvider.createAuthenticationRequest()`, 또는 wire trace(HAR) | `needs-confirmation` (research opt-in) |
| **[Hop2 — gap]** Keycloak 기본 서명 알고리즘이 RS256 인가 | `KC-ROT-C1` 은 "자체 active key 서명"만 증명, 알고리즘 명시 없음 | Keycloak realm "Keys" 탭 문서 또는 realm `default-signature-algorithm` provider config 확인 | `needs-confirmation` |
## 마주친 문제
- (학습 단계, 미실행)
- **잠재적 함정 기록**:
- ~~Google이 issuer 표기를 `https://accounts.google.com`로도 `accounts.google.com`로도 발급한 사례가 있음 (과거).~~ ⚠️ **정정(2026-07-18)**: 이 진술은 **미검증 folklore** 다. 아카이브 근거(`#GOOGLE-OIDC-C1`)는 `iss` 를 정확히 `https://accounts.google.com` 단일 문자열로 규정하고 alias 를 부인한다. 둘 다 유효했다는 2차 아카이브(예: 과거 Google OIDC discovery 스냅샷)가 나오기 전엔 "둘 다 허용" 으로 취급 금지. (CV1)
- Keycloak이 Google JWKS를 가져오지 못하면 (네트워크 단절, Google 측 변경) Google 로그인 전체가 5xx. 모니터링 alarm 필요. (§엣지·실패·의존, D2/CV3)
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/official-docs/openid-connect-core-id-token-validation]]
<!-- 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`). 단 D2·CV2·CV3·CV6·Hop2 gap 종결 후 `wiki/concepts/keycloak-google-federation-trust-boundary` (hop 검증 매트릭스) 합성 후보(현 status `documented-only` 로 파생 게이트 미달).