467 lines
60 KiB
Markdown
467 lines
60 KiB
Markdown
---
|
|
title: branch / feature-keycloak-internal-spa-direct-no-google (P2A Internal SPA + Resource Server, no Google)
|
|
source_type: branch-note
|
|
status: raw
|
|
id: BR-KEYCLOAK-CHILD-D594F009
|
|
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-internal-spa-direct-no-google
|
|
parent_branch: feature-keycloak-patterns
|
|
related_projects: [keycloak-patterns]
|
|
tags: [branch, keycloak-patterns, auth, oauth2, oidc]
|
|
created: 2026-05-25
|
|
target_merge:
|
|
status_label: in-progress
|
|
contract_packet_sha256: 118c42959d56467a19dd0f6cc00f7c9c6f5fec89851f3bff46970ef6c12d4cbf
|
|
---
|
|
|
|
# branch: feature-keycloak-internal-spa-direct-no-google — P2A Internal SPA + Resource Server (no Google)
|
|
|
|
> Layer: `raw/branch-notes/` — [[raw/branch-notes/feature-keycloak-patterns]] (root)의 sub-branch.
|
|
> **P2A**: Keycloak이 cluster-internal에 배치되고, SPA(vanilla JS)가 Keycloak에 **직접** OIDC Authorization Code + PKCE로 토큰을 받아옴. 백엔드는 Spring Security Resource Server — JWT 서명·`iss`·`aud`·`exp` 검증만 수행. **Edge proxy 없음.** Google federation 없음.
|
|
> OWASP / OAuth 2.1 권고: SPA + API 패턴의 **표준형**.
|
|
> **축 재편 (2026-07-14)**: hub [[raw/project-notes/keycloak-patterns-overview]] §2.3 에서 P2A → **AP1** (Browser-based OAuth Client = SPA-direct + Resource Server) + cross-cutting `배포=cluster-internal` 로 re-map 됨. 본문의 "P2A" 프레이밍은 Phase 0 표기이며, rename/re-parent 은 hub §2.3 대로 `wiki-doc-author mode=migrate` 로 점진 수행(§진행 중 메모 `AXIS_DRIFT`).
|
|
|
|
<!-- 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 변형이다 | Google 없는 SPA Direct 패턴을 AP1과 internal deployment 변형으로 분류한다 | [[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 -->
|
|
## 목표
|
|
|
|
SPA가 Keycloak에 직접 OIDC + PKCE로 토큰을 받고, 백엔드는 JWT 검증만 하는 패턴을 토큰 교환 sequence와 신뢰 경계 수준까지 명확히 설명할 수 있게 한다.
|
|
|
|
핵심 질문:
|
|
|
|
- **왜 PKCE가 SPA에서 의무인가?** (public client → client secret 보관 불가 → authorization code 탈취 위험 → PKCE로 code-to-token binding)
|
|
- **백엔드는 무엇을 검증해야 하는가?** (signature via JWKS / `iss` / `aud` / `exp`)
|
|
- **token custody policy는 무엇인가?** ([[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 — pure SPA token custody policy)
|
|
- **TMB 대안 경계는 무엇인가?** ([[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D2 — TMB alternative boundary)
|
|
|
|
OAuth 2.1 draft가 implicit flow를 제거하고 PKCE를 모든 authorization code flow에 의무화한 이유를 SPA 관점에서 정리.
|
|
|
|
- 이슈: (학습 노트, 이슈 없음)
|
|
- PR: (구현 없음 — D8 에 의해 문서 전용)
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- **패턴 정의 (본 branch 고유 소유)** — SPA 가 public client 로 직접 OIDC Authorization Code + PKCE 를 수행하고 백엔드는 Resource Server 로 JWT 검증만 하는 경계 확정: *누가 토큰을 보유하고 누가 검증하는가* (D1). 자식 5개와 형제 branch 가 이 정의를 기준선으로 인용한다.
|
|
- **cluster-internal 배치의 URL 경계** — 브라우저가 도달하는 frontchannel public URL 과 백엔드가 JWKS 를 조회하는 backchannel internal URL 의 분리, 그리고 `iss` 를 frontchannel 로 고정해야 하는 이유 (D7). 본 branch 의 배포 축(cluster-internal)에서만 발생하는 고유 관심사.
|
|
- **Google federation 제외 범위 확정** — realm 내부 사용자만 (D4). brokering 비교는 P2B 소관.
|
|
- **문서 산출물** — 토큰 교환 sequence · 신뢰 경계 · P1A 대비 trade-off 표 (모두 `documented-only`).
|
|
- **자식 sub-sub-branch 로의 결정 위임 맵** — PKCE 단계 / 백엔드 validator / 토큰 저장 / rotation / BFF 비교의 owner 지정 (§구현 가이드 §3, `rules/consistency-contract.md` Single-Owner 준수).
|
|
|
|
### 제외 범위
|
|
|
|
> 의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.
|
|
|
|
- **실 구현 / 배포** — hub [[raw/project-notes/keycloak-patterns-overview]] §5 의 고정 결정 F5 에 의해 4 패턴 E2E 는 single-EC2 docker-compose **1벌로만** 구현하고, cluster-internal 은 hostname·issuer·network 차이만 문서화한다 (D8). 실 구현 대상은 [[raw/branch-notes/feature-keycloak-single-ec2-no-google]] (P3A).
|
|
- **PKCE 4단계 메커니즘 상세** (verifier/challenge 생성·검증 공식) — [[raw/branch-notes/feature-keycloak-pkce-flow-stages]] D1 소관.
|
|
- **백엔드 JWT validator 구현 상세** (audience validator, `issuer-uri` wiring, JWKS cache) — [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1 소관.
|
|
- **토큰 저장 위치 상세** (메모리 / cookie / localStorage 비교) — [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 소관.
|
|
- **refresh rotation / revocation 상세** — [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1 소관.
|
|
- **BFF 대안 비교 상세** — [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D1 소관.
|
|
- **Google IdP brokering** — [[raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation]] (P2B) 소관.
|
|
- **인가(RBAC) — role → `@PreAuthorize`** — hub §5 의 deferred(authZ) 트랙. 본 branch 는 authN 토큰 흐름까지만.
|
|
- **`iss` mismatch 함정의 재현·해결 절차** — [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1(`KC_HOSTNAME` 고정) + D4(실패 먼저 재현) + **D6**(해결 메커니즘 선택) 소관(single-EC2 맥락). 본 branch 는 cluster-internal 의 URL *경계 정의*까지만 (D7).
|
|
|
|
## 근거 (필수, 최소 1개+)
|
|
|
|
> 본 sub-branch의 P2A (Internal SPA + Resource Server, no Google) 채택 근거. 상세 대안 비교는 §외부 근거 / 대안 조사 참조.
|
|
|
|
| Source | 정당화하는 결정 |
|
|
|---|---|
|
|
| [[raw/official-docs/oauth2-pkce-rfc-7636]] | RFC 7636 PKCE — public client 필수 PKCE 채택 근거 |
|
|
| [[raw/official-docs/oauth-v2-1-draft-ietf]] | OAuth 2.1 draft — Auth Code + PKCE 채택 근거 |
|
|
| [[raw/official-docs/spring-security-resource-server-jwt]] | Spring Security Resource Server JWT 검증 — backend JWT validator 근거 |
|
|
| [[raw/official-docs/keycloak-securing-apps-overview-official]] | Keycloak Securing Apps overview — 보안 모델 근거 |
|
|
| [[raw/official-docs/owasp-html5-storage-xss-spa]] | OWASP HTML5 Storage + XSS — token 저장 위치 trade-off 근거 |
|
|
| [[raw/company-tech-blogs/curity-bff-pattern-spa]] | Curity BFF pattern — BFF 대안 검토 (참고) |
|
|
| [[raw/official-docs/keycloak-hostname-configuration]] | Keycloak Hostname v2 — cluster-internal 의 frontchannel/backchannel URL 분리 + `iss` 고정 근거 (D7, 2026-07-17 추가) |
|
|
| [[raw/official-docs/keycloak-client-pkce-method-enforcement-official]] | D5 (PKCE S256 강제)의 Keycloak vendor 측 근거 — Admin UI "PKCE method" 옵션의 정확한 명칭/위치/선택지별 동작 (2026-07-17 추가) |
|
|
|
|
## 외부 근거 / 대안 조사 (2026-05-25 — P2A Internal SPA + Resource Server)
|
|
|
|
본 sub-branch의 **SPA Direct OIDC + Backend Resource Server (JWT 검증)** 채택에 대한 외부 source. OAuth 2.1 권고 패턴.
|
|
|
|
- **채택 결정 (Authorization Code Flow + PKCE + Resource Server JWT validation)**:
|
|
- [[raw/official-docs/oauth2-pkce-rfc-7636]] — RFC 7636 PKCE (public client 필수)
|
|
- [[raw/official-docs/oauth-v2-1-draft-ietf]] — OAuth 2.1 draft (PKCE mandatory, implicit grant 제거)
|
|
- [[raw/official-docs/spring-security-resource-server-jwt]] — Spring Security Resource Server JWT 검증 (issuer-uri, JwtDecoder, audience validator 추가 필요)
|
|
- [[raw/official-docs/keycloak-securing-apps-overview-official]] — Keycloak Securing Apps overview
|
|
- [[raw/official-docs/owasp-html5-storage-xss-spa]] — OWASP HTML5 Storage + XSS in SPA (token 저장 위치 고민)
|
|
- [[raw/company-tech-blogs/curity-bff-pattern-spa]] — Curity BFF pattern article
|
|
- **검토한 대안**:
|
|
- **대안 1: Edge ForwardAuth (P1A)** — 백엔드는 인증 코드 0, header 신뢰. 비교 sub-branch: [[raw/branch-notes/feature-keycloak-edge-forwardauth-no-google]].
|
|
- **대안 2: BFF (Backend-for-Frontend)** — 백엔드 session cookie + token 백엔드 보유. 장: XSS surface 축소 (token이 SPA에 노출 안 됨), refresh token rotation 안전 / 단: 백엔드 stateful, scale-out 시 session 공유 (Redis 등) 필요.
|
|
- **대안 3: Implicit Flow** — OAuth 2.1에서 **제거**됨 (token이 URL fragment 노출). 채택 불가.
|
|
- **대안 4: Resource Owner Password Credentials (ROPC)** — 사용자 credentials를 백엔드가 받음. RFC 6749 deprecated. 채택 불가.
|
|
- **대안 5: Hybrid Flow (Authorization Code + ID token in fragment)** — OpenID Connect, ID token 빨리 받음. 그러나 token 노출 위험 + 복잡.
|
|
- **비교 핵심**: SPA Direct OIDC + PKCE는 브라우저가 token custody를 직접 가진다. [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 — pure SPA token custody policy. [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D2 — TMB alternative boundary. audience 검증 방식은 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1을 따른다.
|
|
|
|
## TODO
|
|
|
|
각 항목 옆에 증거 등급: `actually-implemented` | `locally-verified` | `prod-verified` | `documented-only` | `planned` | `needs-confirmation`
|
|
|
|
- [ ] 컴포넌트 다이어그램 (mermaid sequence) — 등급: `planned`
|
|
- [ ] Keycloak SPA client 설정 항목 정리 (public, PKCE S256 enforced, redirect URI, Web Origins) — 등급: `documented-only`
|
|
- [ ] Spring Security Resource Server `application.yml` snippet — 등급: `documented-only`
|
|
- [ ] Audience validator (custom `OAuth2TokenValidator<Jwt>`) 코드 sketch — 등급: `documented-only`
|
|
- [ ] BFF 변형 sequence diagram 추가 — 등급: `planned`
|
|
- [ ] refresh token rotation flow diagram — 등급: `planned`
|
|
- [ ] P1A 대비 trade-off 표 (다이어그램 포함) — 등급: `documented-only`
|
|
- [ ] cluster-internal frontchannel/backchannel URL 경계를 컴포넌트 다이어그램에 반영 (현재 ingress 경로 미표기 — §진행 중 메모 `INGRESS_UNDERSPECIFIED`, D7) — 등급: `planned`
|
|
|
|
## 진행 중 메모
|
|
|
|
작업하며 떠오른 메모. 자유 형식.
|
|
|
|
- **`AXIS_DRIFT` (2026-07-17 `/branch-spec` 확인)** — hub 가 2026-07-14 에 분류 primary 축을 *배치×federation 6패턴* → *인증 아키텍처 4패턴(AP1~AP4)* 로 교정([[raw/project-notes/keycloak-patterns-overview]] §2.3)했으나 본 노트 본문은 여전히 Phase 0 의 "P2A" 프레이밍이다. 매핑은 **P2A → AP1 + 배포=cluster-internal**. hub §2.3 이 "실제 rename/re-parent 은 `wiki-doc-author mode=migrate` 로 점진 수행(자동 mv 금지 — wikilink 영향 검토)"라고 명시하므로 본 `/branch-spec` 회차에서는 **본문 재작성 없이 정합 표기만** 추가했다(제목 blockquote + 본 메모). 실제 re-parent 대상은 hub §2.3 이 지목한 자식 2개(`spring-rs-audience-validator`, `spa-token-storage-tradeoff` → AP1 그룹)이며 현재는 cosmetic 이라 미실행.
|
|
- **본 노트는 pattern hub — 결정 detail 의 owner 가 아니다.** 5개 자식이 각 관심사의 owner 이고([[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] 등), 자식들은 본 노트의 `D1`(SPA Direct = 브라우저 token 보유 정의)을 기준선으로 역참조한다. 반면 본 노트의 `D2`·`D3`·`D5`·`D6` 은 자식 owner 결정의 *요약*이라 `rules/consistency-contract.md` 의 `RESTATED_FOREIGN_DECISION` 소지가 있다 — owner 가 진화하면 낡은 복제본이 된다. 본 회차에서는 사용자 작성 결정을 덮어쓰지 않고(retro 정책: "일괄 자동 수정 금지, `/sync` fix-plan 으로 점진 수거") §구현 가이드 §3 에 **위임 맵**을 세워 포인터를 명시했다.
|
|
- **`INGRESS_UNDERSPECIFIED`** — 본 노트 §컴포넌트 다이어그램은 `Browser (SPA) ─── OIDC ───► Keycloak (cluster-internal)` 로 그려져 있으나, 브라우저는 cluster-internal 서비스에 직접 도달할 수 없다. Authorization Code 흐름은 브라우저가 `/auth` 로 **redirect** 되고 `/token` 을 **직접 fetch** 해야 성립하므로 Keycloak frontchannel 은 외부 도달 가능한 경로(ingress)를 가져야 한다. 즉 "cluster-internal" 은 *edge forward-auth 프록시가 없다*(P1 과의 차이)는 뜻이지 *Keycloak 이 도달 불가*라는 뜻이 아니다. 이 경로가 미표기라 `iss` 함정의 발생 지점이 노트상 보이지 않는다 → D7 로 경계를 명시하고, 다이어그램 반영은 §TODO 에 남김.
|
|
- P1A(Edge ForwardAuth)와의 결정적 차이는 *토큰 보유 주체*다. P1A 는 프록시가 세션을 쥐고 백엔드는 헤더를 신뢰하지만, P2A 는 브라우저가 토큰을 쥐고 백엔드가 JWT 를 직접 검증한다 — 그래서 P2A 는 XSS surface 를, P1A 는 헤더 spoofing 을 각각의 signature 함정으로 갖는다.
|
|
|
|
## 컴포넌트 다이어그램
|
|
|
|
```text
|
|
┌──────────────────┐
|
|
│ Keycloak │
|
|
│ (cluster- │
|
|
│ internal) │
|
|
│ │
|
|
Browser (SPA, vanilla JS) ─── OIDC ────────►│ /auth /token │
|
|
◄── tokens ───────│ /certs (JWKS) │
|
|
└──────────────────┘
|
|
▲ JWKS fetch (캐싱)
|
|
│
|
|
Browser ── Authorization: Bearer <access_token> ─► │
|
|
┌────────┴─────────┐
|
|
│ Backend │
|
|
│ Spring Security │
|
|
│ Resource Server │
|
|
│ (JWT validate) │
|
|
└──────────────────┘
|
|
```
|
|
|
|
> ⚠️ 위 다이어그램은 브라우저 → Keycloak 의 **ingress 경로를 생략**하고 있다(§진행 중 메모 `INGRESS_UNDERSPECIFIED`). 실제 경계는 §구현 가이드 §2 (D7) 참조 — 브라우저는 frontchannel public URL 로, 백엔드는 backchannel internal URL 로 같은 Keycloak 에 도달한다.
|
|
|
|
신뢰 경계 (trust boundary):
|
|
|
|
- **SPA**: public client. token sink. 사용자 브라우저 환경 — XSS가 발생하면 토큰 노출.
|
|
- **Keycloak**: Authorization Server. 토큰 발급 / JWKS publish.
|
|
- **Backend**: Resource Server. **SPA를 신뢰하지 않음** — 모든 요청의 JWT를 signature + `iss` + `aud` + `exp`까지 직접 검증해야 SPA 우회 공격 방지.
|
|
|
|
## 토큰 교환 sequence (Authorization Code + PKCE)
|
|
|
|
1. **PKCE 준비 (SPA)**:
|
|
- `code_verifier`: 43~128 byte random string (RFC 7636 §4.1).
|
|
- `code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))` (S256 method).
|
|
- `state`, `nonce` random 값 생성 (CSRF/replay 방지).
|
|
2. **Authorization Request (SPA → Keycloak)**:
|
|
```text
|
|
GET /realms/<realm>/protocol/openid-connect/auth
|
|
?response_type=code
|
|
&client_id=<spa-client>
|
|
&redirect_uri=<SPA URL>
|
|
&scope=openid profile email
|
|
&state=<random>
|
|
&code_challenge=<challenge>
|
|
&code_challenge_method=S256
|
|
```
|
|
3. **사용자 로그인** → Keycloak이 redirect with `?code=<auth_code>&state=...`.
|
|
4. **Token Request (SPA → Keycloak)**:
|
|
```text
|
|
POST /realms/<realm>/protocol/openid-connect/token
|
|
grant_type=authorization_code
|
|
code=<auth_code>
|
|
redirect_uri=<SPA URL>
|
|
client_id=<spa-client>
|
|
code_verifier=<verifier> ← Keycloak이 SHA256 후 step 2의 challenge와 비교
|
|
```
|
|
응답: `access_token` (JWT) / `id_token` (JWT) / `refresh_token` / `expires_in`.
|
|
5. **토큰 저장 (SPA)**:
|
|
- [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 — pure SPA token custody policy.
|
|
- [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D2 — TMB alternative boundary.
|
|
6. **API 호출 (SPA → Backend)**:
|
|
```text
|
|
GET /api/...
|
|
Authorization: Bearer <access_token>
|
|
```
|
|
7. **JWT 검증 (Backend, Spring Security Resource Server)**:
|
|
- JWKS endpoint(`/realms/<realm>/protocol/openid-connect/certs`)에서 public key fetch + 캐싱.
|
|
- signature 검증 (`kid` 매칭).
|
|
- `iss` claim = `https://<keycloak>/realms/<realm>` 일치.
|
|
- `aud` claim에 backend client id 포함 (custom `OAuth2TokenValidator` 추가 필요).
|
|
- `exp` / `nbf` 시간 검증 (기본 clock skew 60s).
|
|
8. **Refresh** (access_token 만료 시): SPA → Keycloak `/token` (`grant_type=refresh_token`) → 새 access_token (+ rotated refresh_token).
|
|
|
|
## 장점 / 단점 vs P1A (Edge Forward Auth)
|
|
|
|
| 항목 | P2A (Internal SPA + Resource Server) | P1A (Edge ForwardAuth) |
|
|
|------|--------------------------------------|------------------------|
|
|
| 백엔드 상태 | **Stateless** (JWT만 검증) | 보통 stateless이나 프록시가 세션 보유 가능 |
|
|
| 토큰 위치 | [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 — pure SPA token custody policy | 프록시 session policy는 비교 패턴 owner 참조 |
|
|
| XSS surface | **높음** (브라우저에 token) | 낮음 (httpOnly session cookie) |
|
|
| 다중 클라이언트 (모바일/IoT) | 동일 access_token 재사용 — **간단** | 모바일은 별도 흐름 필요 |
|
|
| CORS | 명확 (SPA ↔ Backend 직접) | 프록시 뒤로 가려져 단순 |
|
|
| 토큰 revocation | 어려움 (JWT stateless — short TTL + refresh rotation에 의존) | 프록시 session 종료로 즉시 |
|
|
| Keycloak 의존도 | 런타임 JWKS fetch만 (장애 영향 작음) | 프록시 ↔ Keycloak 연결 끊기면 전체 차단 |
|
|
| 운영 복잡도 | 낮음 (백엔드 1개 + Keycloak) | 중간 (oauth2-proxy/Traefik 추가) |
|
|
|
|
## 신뢰 경계 / 보안 체크리스트
|
|
|
|
- [ ] **PKCE S256 의무**: public client는 `code_challenge_method=plain` 금지. Keycloak client 설정에서 **`PKCE method` = S256** 강제 (Admin Console → Basic configuration → **Capability Config**). ※ 2026-07-17 정정 — 이 항목은 원래 라벨을 `Proof Key for Code Exchange Code Challenge Method` 로 적었으나 `KC-PKCE-C1` 확인 결과 **부정확**. 값을 비워두면(기본) Keycloak 은 PKCE 를 **강제하지 않는다**(`KC-PKCE-C2`) — 즉 "public client 니까 자동 적용"이 아니라 client 마다 명시 설정이 필요하다. 단 S256 설정이 `plain` 요청을 실제로 *거부*한다는 문장은 공식 문서에 없음 → §Claims To Verify.
|
|
- [ ] **`aud` 검증**: Spring Security 기본 validator는 `iss` + `exp`만 확인. `audience` claim은 **반드시 custom `OAuth2TokenValidator`로 추가** 검증 (cross-client token reuse 방지).
|
|
- [ ] **`iss` 검증**: `spring.security.oauth2.resourceserver.jwt.issuer-uri`로 자동 검증.
|
|
- [ ] **JWKS 캐싱 + 키 로테이션**: 기본 5분 캐시. Keycloak 키 회전 시 자동 갱신.
|
|
- [ ] **redirect_uri exact match**: Keycloak client 설정에 exact URI 등록. ※ 2026-07-17 정정 — 원래 근거를 "RFC 8252 권고"로 적었으나 RFC 8252 는 *native app* scope 이고, 본 패턴(브라우저 SPA)의 정확한 근거는 이미 본 branch Sources 안에 있는 **`OA21-C5` (OAuth 2.1 §2.3.1) — "권고"가 아니라 `MUST`**: *"Authorization servers MUST reject authorization requests that specify a redirect URI that doesn't exactly match one that was registered."* 등록 제약의 구체 명세는 §구현 가이드 §2 (D7).
|
|
- [ ] **`state` / `nonce` 검증** (SPA): CSRF / replay 방지.
|
|
- [ ] **refresh token rotation**: Keycloak Realm Settings → `Revoke Refresh Token: ON` + `Refresh Token Max Reuse: 0`.
|
|
- [ ] **access_token TTL 짧게**: 5~15분. JWT revocation이 어려우므로 짧은 TTL로 보완.
|
|
- [ ] **CORS 화이트리스트**: backend가 `Access-Control-Allow-Origin`에 SPA origin만 허용.
|
|
|
|
## PKCE 의무 (OAuth 2.1
|
|
|
|
OAuth 2.1 draft: *"Clients MUST use code_challenge and code_verifier and authorization servers MUST enforce their use except under the conditions described in Section 7.5.1."* — public client뿐 아니라 모든 client에 의무화. implicit flow는 제거됨.
|
|
|
|
RFC 7636 §1: *"OAuth 2.0 public clients utilizing the Authorization Code Grant are susceptible to the authorization code interception attack."* — 브라우저 redirect 단계에서 code가 노출될 수 있고, public client는 client secret이 없으므로 code만 탈취되면 토큰 발급 가능. PKCE는 code-to-token 단계에 verifier 증명을 요구하여 이 공격을 차단.
|
|
|
|
## refresh token rotation
|
|
|
|
- Keycloak: Realm Settings → Tokens 탭
|
|
- `Revoke Refresh Token`: ON
|
|
- `Refresh Token Max Reuse`: 0 (한 번 쓰면 무효)
|
|
- `SSO Session Idle`: 짧게
|
|
- 효과: refresh token이 탈취되어도 한 번만 사용 가능. 정상 사용자가 다음 refresh를 시도하면 양쪽 다 거부됨 → 침해 탐지 시그널.
|
|
- OAuth 2.1: *"If refresh tokens are issued, those refresh tokens MUST be bound to the scope and resource servers as consented by the resource owner."*
|
|
|
|
## BFF (Backend-for-Frontend) 비교
|
|
|
|
SPA가 직접 토큰을 보유하지 않고 백엔드(BFF)가 OAuth client 역할을 대신 수행하는 변형.
|
|
|
|
| 항목 | SPA Direct (본 P2A) | BFF 변형 |
|
|
|------|---------------------|---------|
|
|
| 토큰 보관 | [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 — pure SPA token custody policy | 비교 패턴 owner의 server-side custody policy 참조 |
|
|
| 브라우저 ↔ Backend | `Authorization: Bearer <jwt>` | **httpOnly session cookie** |
|
|
| XSS로 bearer token 직접 탈취 | 실행 중인 JS memory에서 가능 | bearer token은 브라우저 JS에 노출되지 않음. 단 XSS가 활성 session으로 요청을 대행할 위험은 남음 |
|
|
| 백엔드 상태 | stateless | **stateful** (session store) |
|
|
| 다중 클라이언트 (모바일) | 동일 흐름 | 모바일은 별도 OAuth client 필요 |
|
|
| 권장 (Curity, OAuth 2.1 draft) | 허용 | **권장** (특히 민감 데이터) |
|
|
|
|
OAuth 2.1 draft: SPA가 "wish to use client credentials"인 경우 *"the backend for frontend pattern"*을 권고. Curity: *"The only way to protect tokens from being accessed by any malicious code is to keep them away from the browser."*
|
|
|
|
본 branch는 SPA Direct 흐름을 학습 목적으로 채택 (canonical OIDC + PKCE 흐름 이해가 우선). BFF는 비교 문서로만 정리.
|
|
|
|
## 결정 사항 (decisions)
|
|
|
|
- 2026-05-25: P2A는 **SPA Direct (토큰을 브라우저에 보유)**로 정의. BFF는 별도 변형으로 비교만. 이유: 가장 canonical한 OIDC + PKCE 흐름을 먼저 이해하기 위함.
|
|
- 2026-07-18: [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 — pure SPA token custody policy. [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D2 — TMB alternative boundary.
|
|
- 2026-05-25: 백엔드 검증은 **`iss` + signature + `exp` + `aud` 4종**. Spring Security 기본에 audience validator를 반드시 추가.
|
|
- 2026-05-25: Google federation 없음 — Keycloak realm 내부 사용자만. brokering 확장의 zero-change invariant는 [[raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation]] D8이 소유한다.
|
|
- 2026-07-17: cluster-internal 배치에서 브라우저는 **frontchannel public URL**, 백엔드 JWKS 조회는 **backchannel internal URL** 로 분리하고 `iss` 는 frontchannel 로 고정. 이유: Keycloak 이 frontchannel/backchannel URL 분리를 공식 지원하며(KC-HOST-C1), hostname 미고정 시 fraudulent issuer 위험(KC-HOST-C3). 검토한 대안: (a) 브라우저·백엔드 모두 internal DNS → 브라우저 도달 불가 (b) 양쪽 모두 public URL → 백엔드가 불필요하게 ingress 왕복.
|
|
- 2026-07-17: 본 branch 는 **`documented-only` 유지** — 실 구현은 hub 고정 결정 F5 에 의해 single-EC2(P3A)로 위임. 이유: 배포 토폴로지는 cross-cutting 이라 인증 아키텍처를 바꾸지 않으므로 실 구현 1벌로 4 패턴 검증이 성립.
|
|
|
|
## 결정-근거 매핑
|
|
|
|
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지. P2A 는 OAuth 2.1 표준 권고 패턴이라 대부분 `official-standard` 근거.
|
|
|
|
> `선택 조건` 열(R2, 2026-07-17 추가): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 `N/A`.
|
|
>
|
|
> **Ownership note** — 본 노트는 pattern hub 다. `D1`·`D4`·`D7`·`D8` 만 본 branch 고유 소유이고, `D2`·`D3`·`D5`·`D6` 은 자식 owner 결정의 요약이다(§구현 가이드 §3 위임 맵 · §진행 중 메모 `RESTATED_FOREIGN_DECISION`). 세부는 owner 를 정본으로 본다.
|
|
|
|
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|---|
|
|
| D1 | P2A 를 SPA Direct (브라우저가 token 보유) 로 정의, BFF 는 비교만 | **canonical OIDC + PKCE 흐름 학습이 1차 목표**이거나 모바일/IoT 까지 동일 token 흐름을 재사용해야 하면 SPA Direct. **XSS 민감 데이터(금융/의료)** 이거나 SPA 가 **client credentials 를 써야 하면**(`OA21-C4` 의 §2.1 조건) BFF(AP3)로 전환 — 자식 [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D1 이 같은 분기를 owner 로 상술 | `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C1`, `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C4`, `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C1`, `raw/company-tech-blogs/curity-bff-pattern-spa` (참고) | `official-standard + official-standard + official-standard + company-case-study` | `OA21-C4` 는 BFF 가 "recommended" 라고 명시 — SPA Direct 채택은 canonical 학습 우선순위 기반 trade-off. company-tech-blog (Curity) 는 보조 참고지 best practice 단정 근거 아님 |
|
|
| D2 | **DELEGATED** — [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 — pure SPA token custody policy | [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D2 — TMB alternative boundary | owner 참조 | `delegated` | owner의 custody risk가 본 패턴에도 적용됨 |
|
|
| D3 | 백엔드 검증 = `iss` + signature + `exp` + `aud` 4종 (Spring Security 기본에 audience validator 반드시 추가) | **백엔드가 JWT 를 직접 신뢰하는 모든 경우**(AP1 = 본 패턴). 대안은 백엔드가 검증을 아예 안 하는 AP4 Edge forward-auth — 인증을 프록시에 위임하고 헤더를 신뢰할 때만 성립(hub §2.1). 즉 "검증 생략"은 배치를 바꿔야 얻는 선택지지 본 패턴 내 옵션이 아님 | `raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C1`, `raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C2`, `raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C6` | `official-vendor-doc` | `SSRS-JWT-C6` 는 Boot `audiences` property 가 `aud` 검증을 활성화함을 보장. 그러나 본 결정의 "custom `OAuth2TokenValidator` 로 추가" 는 별도 §Configuring Validation 페이지 (인용 범위 밖) — programmatic 방식 검증 필요. **owner 는 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1** — 본 행은 요약 |
|
|
| D4 | Google federation 없음 — Keycloak realm 내부 사용자만. 확장 시 [[raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation]] D8의 zero-change invariant를 consume | 학습 범위를 realm 내부 사용자로 한정할 때. Google 계정 로그인이 요구되면 P2B로 확장 | `UNSUPPORTED_DECISION` (scope) + owner D8 pointer | `internal-convention + delegated` | 코드 변경량 0은 본 branch가 재결정하지 않으며 owner의 diff 검증 전까지 `planned` |
|
|
| D5 | PKCE S256 의무 (`code_challenge_method=plain` 금지). Keycloak client 설정에서 PKCE method = S256 강제 | **public client(브라우저 SPA)** 이면 PKCE 자체는 `OA21-C1` 상 조건 없는 MUST. `plain` 은 S256 을 계산할 수 없는 제약 클라이언트에서만 논의 대상이며 브라우저(`crypto.subtle`)에는 해당 없음 — **단 이는 frontchannel = HTTPS 전제에 종속** (`crypto.subtle` 은 secure context 에서만 노출, `localhost` 예외 — §구현 가이드 §2 의 `UNSUPPORTED_IMPL_DECISION` 참조). 그리고 **"plain 금지"의 직접 근거는 아래 Open Risk 참조** | `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C1`, `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C3`, `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C1`, `raw/official-docs/keycloak-client-pkce-method-enforcement-official.md#KC-PKCE-C1` | `official-standard` (**부분** — 아래 참조) + `official-vendor-doc` | ⚠️ **EVIDENCE_GAP (2026-07-17 확인, 2026-07-17 부분 해소)**: 인용된 claim 중 어느 것도 "plain 금지 / S256 강제 시 실제 거부"를 증명하지 않는다. `OA21-C1` 은 *PKCE 사용* MUST 일 뿐 method 를 S256 으로 한정하지 않고, `PKCE-RFC7636-C3` 은 S256 *공식*만 제공. Keycloak client 의 강제 옵션(Admin UI 경로/속성명)은 이제 `KC-PKCE-C1`이 커버 — 정식 라벨은 "PKCE method"(Capability Config 섹션)이며 기존 추정 라벨("Proof Key for Code Exchange Code Challenge Method")은 부정확했음이 확인됨. **그러나 S256 설정 시 `plain` 요청을 실제로 거부한다는 문장은 Keycloak 공식 문서에도 없음**(`KC-PKCE-C3` Does not prove) — hands-on 검증 필요, `needs-confirmation` 유지. RFC 7636 §4.2 의 MTI 규정은 여전히 미인용. **owner 는 [[raw/branch-notes/feature-keycloak-pkce-flow-stages]] D1** — 본 행은 요약 |
|
|
| D6 | refresh token rotation (Revoke Refresh Token: ON + Max Reuse: 0 + 짧은 SSO Session Idle) | **refresh token 을 발급하는 모든 경우**. rotation 없이 장기 refresh 를 두면 탈취 시 만료까지 무기한 재사용 가능(`CURITY-BFF-C6` 이 SPA Direct 의 핵심 위험으로 지목) → 본 패턴에선 대안 없음. rotation 자체가 불필요해지는 유일한 경로는 refresh 를 브라우저에서 제거하는 AP2/AP3 전환 | `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3` | `official-standard` | `OA21-C3` 은 rotation 빈도/만료 수치를 보장 안 함 → Keycloak 의 실 동작 (한 번 재사용 시 양쪽 token 무효) 은 별도 검증 필요. 본 sub-branch 는 `documented-only`. **owner 는 [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1** — 본 행은 요약 |
|
|
| D7 | cluster-internal 배치의 URL 경계 = 브라우저는 **frontchannel public URL**(ingress), 백엔드 JWKS 는 **backchannel internal URL**, `iss` 는 frontchannel 로 고정 | **Keycloak 이 cluster-internal 이고 브라우저가 직접 OIDC 를 수행하는 본 패턴**에서 적용. 대안 (a) 양쪽 모두 internal DNS → 브라우저가 `/auth` redirect 에 도달 불가하여 흐름 자체가 성립 안 함 (b) 양쪽 모두 public URL → 동작하지만 백엔드 JWKS 가 불필요하게 ingress 를 왕복(`KC-HOST-C1` 이 분리를 지원하는 이유). single-EC2 배포(P3A)면 `KC_HOSTNAME` 단일 host 로 축약 | `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C1`, `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C2`, `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C3`, `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C4`, `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C5` (redirect_uri exact-match MUST) | `official-vendor-doc + official-standard` | ⚠️ **중심 명제는 추론 (2026-07-17 depth 감사)**: "`iss` 를 frontchannel 로 고정"은 `KC-HOST-C1`~`C4` 중 **어느 것도 직접 말하지 않는다** — 세 claim 은 분리 capability(C1) / hostname 의무(C2) / fraudulent issuer rationale(C3) / full URL 요구(C4)까지만 보장한다. `iss` ← `KC_HOSTNAME` + realm path 결합 규칙은 해당 raw 의 Usage Boundaries 가 "본 페이지엔 명시 없음"으로 못박았고, 그 결합을 서술한 절도 "자료 직접 인용 아님"으로 자기표시 → **disclosed inference**, §Claims To Verify 로 검증. `KC-HOST-C1` 은 분리 *가능성*만 보장하고 k8s ingress 의 구체 매니페스트는 범위 밖. 함정의 재현·해결은 [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1 소관(single-EC2 맥락). `redirect_uri` exact-match 는 owner 부재로 **본 D7 이 흡수**(§구현 가이드 §2) |
|
|
| D8 | 본 branch 는 `documented-only` 유지 — 실 구현은 single-EC2(P3A)로 위임 | **배포 토폴로지가 인증 아키텍처를 바꾸지 않는 한**(hub §2.2) 실 구현 1벌로 4 패턴 검증. cluster-internal 고유의 실패(예: ingress 경유 `iss` 불일치)를 E2E 로 재현해야 할 요구가 생기면 별도 k8s 환경 branch 로 승격 | `UNSUPPORTED_DECISION` (외부 raw source 없음 — 내부 규약 [[raw/project-notes/keycloak-patterns-overview]] §5 고정 결정 F5 + §2.2 cross-cutting 정의에 근거) | `internal-convention` | F5 는 "auth 아키텍처를 바꾸지 않는다"는 전제 위에 서 있다. D7 이 지적한 frontchannel/backchannel 분리는 single-EC2 에선 축약되므로, **cluster-internal 고유 함정은 E2E 로 검증되지 않은 채 문서로만 남는다** — 면접에서 "직접 해봤나" 질문에 `documented-only` 로 답해야 함 |
|
|
|
|
## 구현 가이드
|
|
|
|
> 본 branch 는 `documented-only` pattern hub (D8) — 실행 코드가 아니라 **패턴 경계의 사전 명세 + 자식 owner 로의 위임 맵**이 산출물이다. 아래 sub-section 은 본 branch 고유 결정(D1·D4·D7·D8)에서만 도출하며, 자식이 owner 인 detail 은 재진술하지 않고 포인터로 위임한다(`rules/consistency-contract.md` Reference-Only). 실 구현 등급은 모두 `planned`.
|
|
|
|
### 1. P2A 패턴 경계 명세 — 누가 토큰을 보유하고 누가 검증하는가
|
|
|
|
> **Trace**: D1 (SPA Direct 정의 — `OA21-C1`/`PKCE-RFC7636-C1`) + D3 (백엔드 4종 검증 — `SSRS-JWT-C1`/`C2`/`C6`). 본 §가 자식 5개와 형제 branch 가 인용하는 **기준선**이다 — [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D1 이 본 D1 을 SPA Direct 측 기준으로 역참조한다.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 본 표의 각 행은 인용된 claim 의 직접 도출이다.
|
|
|
|
| 경계 | 무엇을 보유 / 수행 | 메커니즘 | 근거 | 등급 |
|
|
|---|---|---|---|---|
|
|
| 브라우저 (SPA) | access/refresh/id token **전부 보유** — public client (client secret 없음) | Authorization Code + PKCE 를 SPA 가 직접 수행. secret 이 없으므로 code 탈취 방어를 PKCE 가 대신함 | `PKCE-RFC7636-C1` (public client 는 code interception 에 취약), `OA21-C1` (PKCE MUST) | `documented-only` |
|
|
| Keycloak (AS) | 토큰 발급 + JWKS publish | `/auth` → `/token` → `/certs` | `KC-SECAPP-C1`~`C2` (Keycloak 보안 모델), `KC-HOST-C2` (hostname 고정) ※ 2026-07-17 축소 — 원래 `C1`~`C3` 로 인용했으나 `KC-SECAPP-C3` 은 *verbatim 원문 부재* 자체가 claim 인 행(strength `needs-confirmation`)이라 **긍정 근거로 인용 불가** | `documented-only` |
|
|
| 백엔드 (Resource Server) | **토큰 미보유** — 요청마다 JWT 를 검증만 | `issuer-uri` 한 줄로 discovery + JWKS fetch + `iss`/`exp` 자동 검증, `aud` 는 별도 추가 | `SSRS-JWT-C1`, `SSRS-JWT-C2`, `SSRS-JWT-C6` | `documented-only` |
|
|
| 백엔드 ↔ SPA | 신뢰 없음 — Bearer JWT 만 | `Authorization: Bearer <access_token>`, 백엔드는 SPA 의 어떤 주장도 검증 없이 수용하지 않음 | D1 (패턴 정의) + `SSRS-JWT-C1` | `documented-only` |
|
|
|
|
### 2. cluster-internal 배치의 URL 경계 (frontchannel vs backchannel)
|
|
|
|
> **Trace**: D7 (`KC-HOST-C1` frontchannel/backchannel 분리 지원, `KC-HOST-C2` hostname 의무·dynamic resolution 차단, `KC-HOST-C3` fraudulent issuer 방어). 본 §는 **본 branch 의 배포 축(cluster-internal)에서만 발생하는 고유 관심사**이며, §진행 중 메모 `INGRESS_UNDERSPECIFIED` 를 종결한다. `KC-HOST-C1` 의 "Applies to" 가 "container 내부 vs 외부 access 가 다른 토폴로지 (예: docker-compose, **k8s**)" 로 본 배치를 직접 지목한다.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: ingress 구현체 선택(k8s Ingress / Gateway API / Service `type=LoadBalancer`)은 어느 인용도 권고하지 않는다. trade-off: 본 branch 는 `documented-only`(D8)라 구현체를 고르지 않고 *경계의 존재*만 확정한다 — 실 구현 시 P3A 는 이 경계가 단일 host 로 축약되므로 선택 자체가 소멸한다.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: 아래 옵션의 정확한 값 형태(hostname-only vs full URL)는 `hostname-backchannel-dynamic` 활성 여부에 종속되며(`KC-HOST-C4` — "If set to true, `hostname` option needs to be specified as a full URL"), 본 branch 는 실 설정을 하지 않으므로 형태(shape)만 기록한다.
|
|
> - **UNSUPPORTED_IMPL_DECISION**: frontchannel 의 scheme(HTTPS 전제)은 인용이 강제하지 않는다. trade-off: SPA 가 S256 challenge 를 계산하는 `crypto.subtle` 은 브라우저 **secure context** 에서만 노출되므로 frontchannel 이 평문 HTTP 면 D5 의 "브라우저는 항상 S256 계산 가능" 전제가 깨진다 — 학습 환경의 `localhost` 예외를 제외하면 frontchannel = HTTPS 로 둔다. 본 corpus 에 이 브라우저 제약의 직접 인용 없음(`oauth2-pkce-rfc-7636.md` Usage Boundaries 도 "추가 확인 필요"로만 기록).
|
|
>
|
|
> **⚠️ 인용 경계 (2026-07-17 depth 감사 반영)**: 아래 `iss` 행의 중심 명제(**`iss` ← frontchannel URL**)는 `KC-HOST-C1`~`C3` 중 **어느 것도 직접 말하지 않는다** — 세 claim 은 (분리 capability / hostname 의무 / fraudulent issuer rationale)까지만 보장한다. `iss` 가 `KC_HOSTNAME` + realm path 로 결합되는 규칙은 해당 raw 의 Usage Boundaries 가 "본 페이지에선 명시 없음"으로 못박았고, 노트가 그 결합을 서술한 절도 "자료 직접 인용 아님"으로 표시돼 있다. 따라서 이 행은 **disclosed inference(추론)** 이며 §Claims To Verify 로 검증 대상이다 — 인용된 사실로 취급 금지.
|
|
|
|
| 경로 | 누가 사용 | 어떤 URL | 근거 | 등급 |
|
|
|---|---|---|---|---|
|
|
| frontchannel | **브라우저** — `/auth` redirect + `/token` fetch + `redirect_uri` 복귀 | 외부 도달 가능한 public URL (ingress 경유). Keycloak `hostname` 옵션으로 **명시 고정** | `KC-HOST-C1` (public URL for frontchannel), `KC-HOST-C2` (hostname 의무), `KC-HOST-C4` (backchannel-dynamic 시 full URL) | `planned` |
|
|
| backchannel | **백엔드** — JWKS(`/certs`) 조회 | cluster 내부 service DNS (ingress 미경유) | `KC-HOST-C1` ("enabling internal communication while maintaining the use of a public URL for frontchannel requests") | `planned` |
|
|
| `iss` claim | 토큰에 각인 → 백엔드가 대조 | **frontchannel URL 로 고정** — 브라우저가 받은 토큰의 발급자가 frontchannel 이므로 백엔드의 기대 issuer 도 동일해야 함 | ⚠️ **추론** (위 인용 경계 참조) — `KC-HOST-C2`/`C3` 는 hostname 고정의 *의무·이유*까지만 보장 | `planned` |
|
|
| **`redirect_uri` 등록** | **Keycloak client 설정** — 브라우저의 복귀 주소 | **frontchannel public URL 기준의 exact URI**. ingress hostname 이 등록값과 한 글자라도 다르면(scheme·port·trailing slash 포함) authorization request 자체가 거부 | `OA21-C5` (**official-standard MUST** — "Authorization servers MUST reject authorization requests that specify a redirect URI that doesn't exactly match one that was registered") | `planned` |
|
|
| **Web Origins (CORS)** | **Keycloak client 설정** — SPA 가 `/token` 을 fetch 할 origin | SPA 를 서빙하는 origin. frontchannel URL 과 **다를 수 있음**(SPA=nginx origin, Keycloak=ingress origin) → 두 origin 이 분리되므로 `/token` 호출이 cross-origin 이 되어 Web Origins 등록 필요 | `UNSUPPORTED_IMPL_DECISION` — Keycloak 의 Web Origins 옵션은 본 corpus 에 미인용(`KC-SECAPP-C1`~`C3` 범위 밖). trade-off: §TODO 가 "Web Origins"를 본 hub 산출물로 지정했고 SPA↔Keycloak origin 분리는 본 배치의 구조적 귀결이라 경계만 기록, 옵션 명세는 실 설정 시 확인 | `planned` |
|
|
| 불일치 시 | 백엔드 401 (`iss`) / Keycloak 거부 (`redirect_uri`) | 백엔드가 backchannel URL 을 기대 issuer 로 설정하면 frontchannel 로 발급된 `iss` 와 mismatch. `redirect_uri` 는 인증 시작 단계에서 즉시 거부 | `iss` 함정 재현·해결은 [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1 + D6 위임 (single-EC2 맥락, 동일 원리). `redirect_uri` 는 **owner 부재 → 본 D7 이 흡수** (아래 참조) | `documented-only` |
|
|
|
|
> **`redirect_uri` 관심사의 owner 귀속 (2026-07-17 depth 감사 — 후보 2개 배제 후 확정)**: SPA↔Keycloak 의 `redirect_uri` exact-match 는 **어느 형제 branch 도 실제로 소유하지 않음**을 전수 확인했다. 후보와 배제 근거:
|
|
>
|
|
> 1. [[raw/branch-notes/feature-keycloak-google-redirect-uri-policy]] D1 — *Google 측* Authorized redirect URI(`/broker/google/endpoint`) 소관. Google 이 검증하는 URI 이지 Keycloak 이 SPA 에게 검증하는 URI 가 아니라 **다른 계약**.
|
|
> 2. [[raw/branch-notes/feature-keycloak-docker-compose-stack]] — hub [[raw/project-notes/keycloak-patterns-overview]] 가 "redirect_uri 함정"의 owner 로 **지목하고 있으나**, 그 노트는 `redirect_uri` 를 **한 번도 다루지 않는다**(2026-07-17 grep: 유일한 "redirect" 매치는 healthcheck 커맨드 줄). → hub 의 해당 포인터는 `STALE_OWNER` 이며 실질 owner 부재.
|
|
>
|
|
> 따라서 **본 D7 이 흡수**한다 — frontchannel public URL 이 곧 등록 제약을 결정하므로 D7 의 자연스러운 확장이다. hub 는 이 함정을 P3A 의 "부차 함정"(`localhost` vs `127.0.0.1` mismatch)으로도 지목하고 있어 실 구현 시 동일 원리로 재현된다. **후속(본 branch 밖, `/sync` 대상)**: hub 의 `STALE_OWNER` 포인터를 D7 로 갱신. (~~실 구현 branch [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] D5 에 `OA21-C5` 근거 역참조 연결~~ → **완료 2026-07-18 `/branch-spec`**: 그 branch D5 가 `UNSUPPORTED_DECISION` 에서 `OA21-C5`(redirect URI exact-match MUST) 직접 인용으로 승격됨. 잔여 임의 detail(단일 callback page 분리)만 `UNSUPPORTED_IMPL_DECISION` 로 강등.)
|
|
|
|
### 3. 결정 위임 맵 (자식 owner — Reference-Only)
|
|
|
|
> **Trace**: D2·D3·D5·D6 은 본 hub 가 요약만 보유하고 detail 의 owner 는 자식이다(§진행 중 메모 `RESTATED_FOREIGN_DECISION`). `rules/consistency-contract.md` 의 Single-Owner 에 따라 **세부는 owner 를 정본으로 본다** — 본 표는 포인터 + 1줄 요약만 유지하고 임계값·메커니즘·예외 목록을 재진술하지 않는다.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 각 행은 owner 노트의 실존 `D<n>` 을 가리킨다(2026-07-17 확인).
|
|
|
|
| 관심사 | owner (정본) | owner 결정 | 1줄 요약 (본 hub 의 인용) |
|
|
|---|---|---|---|
|
|
| PKCE 4단계 메커니즘 (verifier/challenge/exchange) | [[raw/branch-notes/feature-keycloak-pkce-flow-stages]] D1 | S256 만 정리 대상, `plain` 은 비교용 1줄 | 본 hub 의 D5 는 이 결정의 요약 — S256 공식·단계별 detail 은 owner 참조 |
|
|
| 백엔드 JWT validator (`aud` 추가 검증) | [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1 | `iss`+signature+`exp`+`aud` 4종 검증, `aud` 는 custom validator 필수 | 본 hub 의 D3 는 이 결정의 요약 — validator 구현 방식은 owner 참조 |
|
|
| Keycloak `aud` claim 주입 | [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D4 | Keycloak 은 `aud` 에 backend client_id 를 자동 포함하지 않음 → SPA client scope 에 Audience mapper 등록 필수 | 본 hub §신뢰 경계 체크리스트의 "`aud` 검증" 항목이 성립하려면 발급 측 설정이 선행 |
|
|
| 토큰 저장 위치 | [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 | pure SPA token custody policy | [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D2 — TMB alternative boundary |
|
|
| refresh rotation / revocation | [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1 | rotation 활성화(`Revoke Refresh Token: ON` + `Max Reuse: 0`) — reuse detection | 본 hub 의 D6 는 이 결정의 요약. access token revocation 즉시성은 owner D2(짧은 TTL) 참조 |
|
|
| BFF 대안 비교 | [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D1 | SPA Direct 를 학습 1순위로 채택, BFF 는 비교 문서로만 | 본 hub §BFF 비교 표의 정본. 결정 기준 매트릭스는 owner §구현 가이드 §3 참조 |
|
|
|
|
## 엣지·실패·의존
|
|
|
|
> R4(깊이 게이트) 캡처용. 본 branch 는 `documented-only`(D8) 이나, 패턴을 실제로 세울 때 부딪힐 실패/엣지와 다른 계약 의존을 미리 열거한다.
|
|
|
|
- **실패·엣지 경로**:
|
|
- **`iss` mismatch (본 배치의 signature 함정)**: 브라우저는 frontchannel URL 로 토큰을 받고 백엔드는 backchannel URL 을 기대 issuer 로 설정하면 모든 요청이 401. 기대 동작: `iss` 를 frontchannel 로 고정(D7)하고 백엔드 `issuer-uri` 도 동일 값. 근거: `KC-HOST-C1`/`C2`/`C3`. 재현·해결 절차는 [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1 + D6 위임 — (2026-07-17) 그 branch **D6** 가 해결 메커니즘의 선택 기준을 정함. 현재 기본은 **(C) Docker `extra_hosts`** 이고, **(F) Spring `issuer-uri`/`jwk-set-uri` 분리**(`SSRS-JWT-C5` — 두 값을 같게 만들 필요 자체가 없음)는 **근거 있는 권고이나 미승인**(owner 인 P3A D3 미갱신). F 가 승인되면 본 D7 의 "`iss` 는 frontchannel 고정 + 백엔드 `issuer-uri` 도 동일 값" 전제와 **양립**한다(백엔드는 `issuer-uri` 를 frontchannel 로 두고 `jwk-set-uri` 만 backchannel 로 분리) — 즉 본 D7 은 F 승인 여부와 무관하게 유효.
|
|
- **`aud` 미검증 → cross-client token reuse**: Spring 기본 validator 는 `aud` 를 보지 않으므로(`SSRS-JWT-C6` 의 범위) 같은 realm 의 다른 client 토큰이 본 백엔드에서 통과. 기대 동작: audience validator 로 401. 위임: [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1 + D4(발급 측 Audience mapper 선행).
|
|
- **XSS 1건 = 세션 전체 탈취**: 브라우저가 token을 쥐는 것이 본 패턴의 정의(D1)이므로 XSS는 owner 정책의 위험을 그대로 가진다. [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 — pure SPA token custody policy. [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D2 — TMB alternative boundary.
|
|
- **refresh token 재사용 탐지**: rotation ON 상태에서 탈취자와 정상 사용자가 같은 refresh 를 쓰면 양쪽 다 거부 → 침해 시그널이자 **정상 사용자의 강제 로그아웃**(가용성 비용). 기대 동작: 재로그인 유도. 위임: [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1.
|
|
- **Keycloak 미가용**: 백엔드는 JWKS 를 캐시하므로 이미 발급된 토큰은 캐시 유효 동안 계속 검증 가능하나(§신뢰 경계 체크리스트의 "기본 5분 캐시" 주장은 **미검증** — [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D5 가 `UNSUPPORTED_DECISION` 으로 명시), 신규 로그인은 즉시 차단. P1A 와 달리 프록시가 없어 기존 요청은 계속 처리됨 — §장점/단점 표의 "Keycloak 의존도: 장애 영향 작음"이 이 뜻.
|
|
- **CORS preflight 실패**: 본 패턴은 SPA 가 백엔드를 **직접** 호출하므로(P1A 는 프록시 뒤라 동일 origin) `Access-Control-Allow-Origin` 화이트리스트가 없으면 브라우저가 요청을 차단. 기대 동작: SPA origin 만 허용. `UNSUPPORTED_IMPL_DECISION` — 본 branch Sources 에 CORS 직접 인용 없음(§신뢰 경계 체크리스트의 분석 통찰). trade-off: 일반 브라우저 동작 원리로 성립하나 official 단정 불가.
|
|
- **ingress 부재 → 흐름 자체 불성립**: Keycloak frontchannel 이 외부 도달 불가하면 `/auth` redirect 단계에서 실패. 기대 동작: ingress 경로 확보(D7). 본 노트 다이어그램이 이 경로를 생략하고 있음(`INGRESS_UNDERSPECIFIED`).
|
|
- **`redirect_uri` mismatch → 인증 시작 단계에서 거부**: `iss` 를 맞춰도 그 앞에서 깨지는 경로다. ingress hostname 이 Keycloak client 에 등록된 URI 와 정확히 일치하지 않으면(scheme·port·trailing slash·`localhost` vs `127.0.0.1` 포함) authorization request 가 거부되고 토큰 교환까지 가지도 못한다. 기대 동작: frontchannel public URL 기준 exact URI 등록(§구현 가이드 §2). 근거: `OA21-C5` (**MUST** — 본 branch Sources 안의 official-standard). **owner 부재 → 본 hub 의 D7 이 흡수**(형제 [[raw/branch-notes/feature-keycloak-google-redirect-uri-policy]] D1 은 Google 측 `/broker/google/endpoint` 소관이라 위임 불가). hub [[raw/project-notes/keycloak-patterns-overview]] 가 같은 함정을 P3A 의 "부차 함정"으로 지목.
|
|
- **`state` / `nonce` 불일치 → callback 단계 중단**: §토큰 교환 sequence step 1 과 §신뢰 경계 체크리스트가 `state`/`nonce` 를 CSRF/replay 방어로 2회 선언하지만, 검증 실패 시 SPA 동작은 미정의였다. 기대 동작: **조용한 재시도 금지** — `state` 불일치는 CSRF 시도의 신호이므로 code 를 교환하지 말고 흐름을 중단 + 재로그인 유도(재시도는 공격자가 심은 code 를 소비시킬 수 있음). `nonce` 는 `id_token` 검증 시 대조. **owner 부재** — §구현 가이드 §3 위임 맵에 해당 관심사가 없고 `UNSUPPORTED_IMPL_DECISION`: 본 branch Sources 에 `state`/`nonce` 실패 처리의 직접 인용 없음(`OA21-C1`~`C6` 는 PKCE·redirect·refresh binding 까지). trade-off: 중단이 보수적 선택이라 채택하되, 근거는 실 구현(P3A [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]]) 시 OAuth 2.1 §7 계열 인용으로 보강 필요.
|
|
|
|
- **다른 계약 의존**:
|
|
- [[raw/project-notes/keycloak-patterns-overview]] §5 의 고정 결정 **F5**(실 구현 배포 = single-EC2 1벌) — 본 branch 의 D8 이 여기에 직접 의존. F5 가 바뀌어 cluster-internal E2E 가 요구되면 D8 이 무효화되고 본 branch 는 실 구현 branch 로 승격.
|
|
- [[raw/project-notes/keycloak-patterns-overview]] §2 의 고정 결정 **F1**(패턴 taxonomy = AP1~AP4) — 본 branch 는 AP1 + 배포=cluster-internal 로 매핑됨. branch 에서 재정의 금지(SSOT 는 hub).
|
|
- [[raw/project-notes/keycloak-patterns-overview]] §5 의 고정 결정 **F3**(단일 realm + 패턴당 client 1개) — D3 의 `aud` 검증이 성립하는 전제. client 를 분리하지 않으면 audience 로 client 를 구분할 수 없음.
|
|
- [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1 + D4 — 본 hub 의 D3 요약이 의존. owner 가 검증 4종 구성이나 Audience mapper 요구를 바꾸면 본 hub 의 §신뢰 경계 체크리스트도 갱신 필요.
|
|
- [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1 — pure SPA token custody policy. 본 hub의 D2와 sequence step 5는 owner 변경 시 포인터 의미만 재확인한다.
|
|
- [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]] D2 — TMB alternative boundary.
|
|
- [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1 + D2 — 본 hub 의 D6 요약이 의존. rotation 정책이 바뀌면 §refresh token rotation 절과 §장점/단점 표의 revocation 행이 영향.
|
|
- [[raw/branch-notes/feature-keycloak-pkce-flow-stages]] D1 — 본 hub 의 D5 요약이 의존. S256 범위 결정이 바뀌면 §PKCE 의무 절 영향.
|
|
- [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1 + D6 — D7 의 함정 재현(D1·D4)·해결(**D6**)을 위임. 그 branch 는 `parent_branch: feature-keycloak-single-ec2-no-google`(P3A) 이지만 hub 분해표상 **AP1 그룹** 이라 본 패턴과 같은 인증 아키텍처를 공유한다.
|
|
- [[raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation]] D8 — brokering zero-change invariant의 owner. 본 D4는 scope와 owner pointer만 유지한다.
|
|
- [[raw/branch-notes/feature-keycloak-edge-forwardauth-no-google]] (P1A) — §장점/단점 표의 비교 대상. 계약 의존은 아니나 "토큰 보유 주체"의 경계 구분 유지 필요.
|
|
|
|
## 검증해야 할 주장
|
|
|
|
> OAuth 2.1 권고는 표준이지만, Spring Security + Keycloak 결합 시 실제 동작은 별도 검증.
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| Spring Boot 의 `spring.security.oauth2.resourceserver.jwt.audiences` property 가 `aud` 검증을 자동 활성화 (programmatic validator 불필요) | `SSRS-JWT-C6` 는 Boot `audiences` property 의 존재를 보장하나, 본 sub-branch 의 결정 D3 은 "custom `OAuth2TokenValidator` 로 audience 추가" 라고 적혀 있음 → 두 방식 중 어느 쪽이 권장인지 불명확 | Spring Boot 3.x sample 에서 `application.yml` 에 `audiences` 만 설정 → 잘못된 audience JWT 제출 시 401 응답 확인 | `needs-confirmation` |
|
|
| **Keycloak client 의 `PKCE method = S256` 설정이 `code_challenge_method=plain` 요청을 실제로 *거부* 하는지** (D5 의 잔여 갭 — 이것만 남았음) | 2026-07-17 확인: Keycloak **공식 문서에도 거부 문장이 없다**. `KC-PKCE-C3` 는 "Keycloak applies to the client PKCE whose code challenge method is S256" 까지만 말하고 rejection semantics(error code / HTTP status)를 서술하지 않으며, 그 raw 의 Does-not-prove 열이 이를 명시. owner [[raw/branch-notes/feature-keycloak-pkce-flow-stages]] D1 의 Open Risk 도 동일 결론 → **문헌으로는 닫히지 않음, hands-on 만이 종결** | Admin Console → Basic configuration → Capability Config 에서 `PKCE method = S256` 설정 후 `code_challenge_method=plain` 으로 authorize request → 거부 여부 + 실제 error code 관찰. 실행 시점: P3A [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] 구현 시 | `needs-confirmation` |
|
|
| RFC 7636 §4.2 의 S256 **MTI(Mandatory To Implement) 규정**이 corpus 에 미인용 — D5 의 "plain 금지" 중 *표준 측* 근거 | 2026-07-17 `/branch-spec` 확인: `OA21-C1` 은 PKCE *사용* MUST 일 뿐 method 한정 아님. `PKCE-RFC7636-C3` 은 공식만 제공하고 해당 raw 의 Does-not-prove 가 "plain method 도 사용 가능"이라고 명시. RFC 원문의 "If the client is capable of using S256, it MUST use S256, as S256 is Mandatory To Implement (MTI) on the server" 문장이 발췌되지 않음 | RFC 7636 §4.2 를 `wiki-source-summarizer` 로 재발췌해 기존 `raw/official-docs/oauth2-pkce-rfc-7636.md` 에 claim(C6) 추가. **단 실행 주체는 본 hub 가 아니라 owner** [[raw/branch-notes/feature-keycloak-pkce-flow-stages]] D1 (Reference-Only — 본 hub 는 요약만 보유) | `needs-confirmation` |
|
|
| Keycloak `Revoke Refresh Token: ON` + `Refresh Token Max Reuse: 0` 가 refresh rotation 을 정확히 한 번만 허용 | `OA21-C3` scope/resource binding 만 표준 — rotation 동작은 Keycloak 구현 결정 | refresh token 두 번 연속 사용 → 두 번째에서 4xx 응답 + access token 도 invalid 화 확인 | `planned` |
|
|
| Spring Security 기본 `JwtDecoder` 의 JWKS 캐싱 TTL = 5분 (key rotation 시 자동 갱신) | `SSRS-JWT-C2` 는 startup 시 discovery 4단계 보장, 캐시 TTL 수치는 본 인용 범위 밖. owner [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D5 도 `UNSUPPORTED_DECISION` 으로 동일 판정 | Keycloak 키 회전 후 5분 이내 backend 가 새 key 로 검증 가능한지 확인 | `needs-confirmation` |
|
|
| oauth2-proxy 대비 SPA Direct 의 token revocation 차이 (proxy session 종료 즉시 vs Keycloak refresh revoke + access token TTL 대기) | 표 비교 자체는 본 sub-branch 의 분석. 공식 비교는 없음 | 두 패턴 모두 구현 후 logout → 즉시 후속 API 호출의 401 발생 시점 비교 | `planned` |
|
|
| `iss` claim 이 `KC_HOSTNAME` + realm path 로 결합되는 정확한 규칙 (D7 의 중심 명제 = **추론**) | `keycloak-hostname-configuration.md` 의 Usage Boundaries 가 "본 페이지에선 명시 없음 — Resource Server 측 검증 동작과 결합" 으로 못박음. D7 은 이 결합을 전제로 `iss` 고정을 주장 | ① `iss` ← `KC_HOSTNAME` 결합 자체는 **single-EC2 로 검증 가능** — P3A [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1 이 이미 소유(`KC_HOSTNAME=localhost` → `iss=http://localhost:8080/realms/...` 관찰). ② 그러나 **frontchannel/backchannel 분리 시 `/.well-known/openid-configuration` 의 `issuer` 가 어느 쪽으로 표시되는지**는 그 분리 토폴로지를 세워야만 확인 가능 — **D8/F5 가 세우지 않기로 한 바로 그 환경**이라 순환 유예 | `needs-confirmation` (**②는 D8/F5 에 의해 무기한 blocked** — cluster-internal 고유 함정이 문서로만 남는다는 D8 Open Risk 의 구체적 실례. F5 가 바뀌면 해제) |
|
|
|
|
## 마주친 문제
|
|
|
|
- Keycloak Securing Apps 메인 URL(`/docs/latest/securing_apps/`)이 404. 대안 URL(`/securing-apps/overview`)로 fallback. 향후 구현 시 정확한 latest URL은 Keycloak release notes에서 재확인 필요.
|
|
|
|
## 묶음 (자식 sub-sub-branches)
|
|
|
|
<!-- GENERATED: sources:start -->
|
|
- [[raw/company-tech-blogs/curity-bff-pattern-spa]]
|
|
- [[raw/official-docs/keycloak-client-pkce-method-enforcement-official]]
|
|
- [[raw/official-docs/keycloak-securing-apps-overview-official]]
|
|
- [[raw/official-docs/oauth-v2-1-draft-ietf]]
|
|
- [[raw/official-docs/oauth2-pkce-rfc-7636]]
|
|
- [[raw/official-docs/owasp-html5-storage-xss-spa]]
|
|
<!-- GENERATED: sources:end -->
|
|
|
|
<!-- GENERATED: branches:start -->
|
|
<!-- GENERATED: branches:end -->
|
|
|
|
- [[raw/branch-notes/feature-keycloak-pkce-flow-stages]]
|
|
- [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]]
|
|
- [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]]
|
|
- [[raw/branch-notes/feature-keycloak-bff-vs-spa-direct]]
|
|
- [[raw/branch-notes/feature-keycloak-refresh-token-rotation]]
|
|
|
|
> Sources는 하단 "외부 근거" 섹션. Errors/Interview/Lectures/Blog 는 현재 없음.
|
|
|
|
## 관련 sub-branch
|
|
|
|
- 상위: [[raw/branch-notes/feature-keycloak-patterns]]
|
|
- 비교 대상: [[raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation]] (P2B — Internal + Google federation)
|
|
- 비교 대상: [[raw/branch-notes/feature-keycloak-edge-forwardauth-no-google]] (P1A — Edge ForwardAuth vs SPA Direct)
|
|
- 구현 대상: [[raw/branch-notes/feature-keycloak-single-ec2-no-google]] (P3A — Single EC2 vanilla JS 구현)
|
|
|
|
## 관련 일일 노트
|
|
|
|
|
|
## 완료 후 정리
|
|
|
|
- PR 링크: (미구현 — 문서까지만)
|
|
- 머지 결과 / 배포 환경: 없음
|
|
- **wiki 추출 대상**: 현 단계 없음. P2A는 `documented-only` 범위 — `wiki/concepts/`로의 추출은 다른 패턴들과 함께 비교 매트릭스가 완성된 뒤에만.
|
|
- **추출하지 않을 항목**: P2A는 본 branch에서 구현 안 함. `actually-implemented`/`locally-verified` 등급의 자체 wiki/projects/ 문서는 생성 불가.
|