--- title: branch / feature-keycloak-single-ec2-no-google (P3A Single EC2 — client + backend + keycloak 동거, no Google) source_type: branch-note status: raw id: BR-KEYCLOAK-CHILD-FE8F0749 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-single-ec2-no-google parent_branch: feature-keycloak-patterns related_projects: [keycloak-patterns] tags: [branch, keycloak-patterns, auth, oauth2, oidc, docker] created: 2026-05-25 target_merge: status_label: in-progress contract_packet_sha256: 930562ffd6f26bab1c938d08a2d2403cdc3627a5b242ffabe05057a32f792e17 --- # branch: feature-keycloak-single-ec2-no-google (P3A Single EC2, no Google) > Layer: `raw/branch-notes/` — [[raw/branch-notes/feature-keycloak-patterns]]의 WI020 child branch. > 본 패턴은 6개 패턴 중 **유일하게 vanilla JS로 실 구현**되는 케이스. 나머지 5개(P1A/P1B/P2A/P2B/P3B)는 문서/다이어그램까지만. > **축 재편 (2026-07-14)**: hub [[raw/project-notes/keycloak-patterns-overview]] §2.3 에서 P3A → **AP1** (Browser-based OAuth Client = SPA-direct + Resource Server) + cross-cutting `배포=single-EC2 (실 구현 base)` 로 re-map 됨. hub 고정 결정 **F5**(§5)가 "E2E 실 구현 배포 = single-EC2 docker-compose **1벌**" 로 확정 → **본 노트는 4 패턴(AP1~AP4) 전체가 얹히는 물리 배포 base** 이다(그 위 실행 단계는 6개 자식이 owner — §구현 가이드 §3). 본문의 "P3A" 프레이밍은 Phase 0 표기이며, rename/re-parent 은 hub §2.3·§8 대로 `wiki-doc-author mode=migrate` 로 점진 수행(§진행 중 메모 `AXIS_DRIFT`). ## 부모 (필수) [[raw/branch-notes/feature-keycloak-patterns]] ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: project governance hub가 AP1~AP4 taxonomy와 child progress index를 유지한다 ### 상속한 프로젝트 결정 | Decision Ref | Project Summary | Branch Application | Source | |---|---|---|---| | `DEC-KEYCLOAK-PATTERNS-OVERVIEW-AUTH-TAXONOMY-001@1` | canonical 분류축은 AP1~AP4 인증 통합 아키텍처와 cross-cutting 변형이다 | single-EC2를 AP1~AP4가 공유하는 deployment cross-cutting 변형으로 분류한다 | [[raw/project-notes/keycloak-patterns-overview]] | ### 브랜치 지역 결정 > 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다. | Decision ID | Decision | Relation | Supporting Claims | Status | |---|---|---|---|---| ### 선언한 예외 | Override ID | Overrides | Reason | Approval | Status | |---|---|---|---|---| 없음. ## 목표 단일 EC2 호스트에 **client (vanilla JS SPA via nginx static)** + **backend (Spring Boot)** + **Keycloak** 세 컴포넌트를 동거시킨 상태에서, OIDC Authorization Code + PKCE 흐름이 실제로 어떻게 동작하는지 코드 레벨로 학습. 토큰 교환 흐름은 P2A와 동일(Browser → Keycloak → Backend Resource Server JWT validation). 차이는 **네트워크 토폴로지**와 **`KC_HOSTNAME` 함정**. 면접에서 "OIDC 전체 lifecycle을 직접 구현해 봤다 → access/refresh/ID token 차이, PKCE 필요 이유, JWT issuer 검증 메커니즘을 코드로 설명 가능"이 목표. ## 범위 ### 포함 범위 - **단일 EC2 배포 토폴로지 확정 (본 branch 고유 소유)** — nginx(SPA static) + Spring Boot(Resource Server) + Keycloak + PostgreSQL 를 **단일 호스트 docker-compose 로 동거**시키는 물리 경계·포트 노출·localhost trust 확정(D3, hub F5). hub 재편 후 **4 패턴(AP1~AP4) E2E 가 모두 이 배포 base 위에 얹힌다**. - **`KC_HOSTNAME` iss 함정의 배포측 정의** — 단일 host 에서 browser 와 backend 가 같은 issuer hostname 을 봐야 하는 이유·해결 축 확정(D3). 재현·해결 절차 자체는 자식 [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] 소관. - **HTTPS-less 학습 경계 확정** (D1) — 학습 환경은 HTTP, prod 진입 시 Caddy / nginx + Let's Encrypt 로 termination 추가. - **vanilla JS 로 OIDC lifecycle 을 실제로 구현하는 유일 케이스** — 6 실 구현 단계(자식)로 분해(§Cluster), 각 단계가 `planned` → 구현 후 `locally-verified` 승급. - **문서 산출물** — 컴포넌트 토폴로지 · 토큰 교환 sequence · 단일 호스트 특이점(`KC_HOSTNAME`/`redirect_uri`) · 장단점 (모두 현재 `planned`/`documented-only`). ### 제외 범위 > 의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거. - **실행 코드 detail** — 본 노트는 6 단계의 **통합 배포 base** 이며 detail 은 재진술하지 않고 위임한다(Reference-Only, §구현 가이드 §3): docker-compose 서비스 정의 → [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D1~D6, realm/client 설정+export → [[raw/branch-notes/feature-keycloak-realm-client-export]] D1~D5, SPA PKCE 코드 → [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] D1~D5, Spring RS 공통 셋업·audience 검증 → [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1/D6, RBAC → [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] D4~D6, iss 재현·해결 → [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1/D4/D6, refresh rotation+logout → [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D1/D5. - **Google IdP federation** — [[raw/branch-notes/feature-keycloak-single-ec2-google-federation]] (P3B) 소관. 본 노트는 no-google base. - **cluster-internal / edge 배포의 별도 구축** — hub F5 에 의해 문서만(hostname·issuer·network 차이). 실 구축(k8s / Traefik)은 안 함. - **AP2/AP3/AP4 인증 아키텍처 자체의 정의** — 본 노트는 *배포 base* 이지 그 패턴 hub 가 아니다. AP1 pattern hub 는 [[raw/branch-notes/feature-keycloak-internal-spa-direct-no-google]] (P2A). - **RBAC 인가 (keycloak role → Spring `@PreAuthorize`)** — hub §5 deferred(authZ). [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] 의 role→role 부분이 여기로 이월. - **prod 배포 / HA cluster / 실제 HTTPS 구성** — 전부 `planned`. 본 회차 범위는 로컬 docker-compose(또는 단일 EC2) 학습 검증까지. ## 근거 (필수, 최소 1개+) > 본 sub-branch의 P3A (Single EC2, 실 구현 대상) 채택 근거. 상세 대안 비교는 §외부 근거 / 대안 조사 참조. | Source | 정당화하는 결정 | |---|---| | [[raw/official-docs/keycloak-server-containers-docker]] | Keycloak Docker container 공식 — docker-compose 채택 근거 | | [[raw/official-docs/keycloak-hostname-configuration]] | Keycloak hostname guide — KC_HOSTNAME 설정 근거 | | [[raw/official-docs/keycloak-getting-started-docker]] | Docker quickstart — 단일 host 학습 구성 근거 | | [[raw/official-docs/spring-security-resource-server-jwt]] | Spring Security Resource Server JWT 검증 — backend Resource Server 근거 | | [[raw/official-docs/oauth2-pkce-rfc-7636]] | RFC 7636 PKCE — public client 필수 PKCE 근거 | | [[raw/official-docs/oidc-client-ts-library]] | oidc-client-ts — vanilla JS OIDC client 라이브러리 선택 근거 | ## 외부 근거 / 대안 조사 (2026-05-25 — P3A Single EC2) 본 sub-branch의 **단일 EC2 (client + backend + keycloak 동거) + Authorization Code + PKCE** 채택에 대한 외부 source. P2A를 단일 호스트로 압축한 형태 + 단일 호스트 고유 함정. - **채택 결정 (Single Host Docker Compose + PKCE + Keycloak `KC_HOSTNAME`)**: - [[raw/official-docs/keycloak-server-containers-docker]] — Keycloak Docker container 공식 (KC_* 환경 변수) - [[raw/official-docs/keycloak-hostname-configuration]] — Keycloak hostname guide (iss claim validation 함정) - [[raw/official-docs/keycloak-getting-started-docker]] — Docker quickstart (단일 host 학습용) - [[raw/official-docs/spring-security-resource-server-jwt]] — Spring Security Resource Server JWT 검증 - [[raw/official-docs/oauth2-pkce-rfc-7636]] — RFC 7636 PKCE (public client 필수) - [[raw/official-docs/oidc-client-ts-library]] — oidc-client-ts (vanilla JS OIDC client 라이브러리 선택지) - **검토한 대안**: - **대안 1: Cluster 배치 (P2A)** — Kubernetes 또는 ECS로 분리 배치. 장: prod-like / 단: 학습 friction 큼 (네트워크 / DNS / cert 모두 관리). 비교 sub-branch: [[raw/branch-notes/feature-keycloak-internal-spa-direct-no-google]]. - **대안 2: Edge ForwardAuth on Single Host** — nginx + oauth2-proxy + Keycloak + backend 모두 단일 host. 장: P1A 학습 가능 / 단: vanilla JS SPA 흐름 학습이 주 목적과 어긋남 (proxy가 인증 처리, SPA는 token 모름). - **대안 3: BFF on Single Host** — Spring Boot이 Keycloak token holder. 장: 보안 우월 (token이 SPA에 없음) / 단: vanilla JS의 OIDC 학습 목적과 어긋남 (SPA가 session cookie만 사용). - **대안 4: Direct host (no Docker)** — Keycloak + Spring Boot + nginx를 EC2에 직접 설치. 장: docker overhead 0 / 단: 환경 reset 어려움, 학습 반복 비용 큼. - **대안 5: 사전 빌드 이미지 (Keycloak Helm + Spring Boot Image)** — managed Keycloak. 학습 단계엔 과함. - **비교 핵심**: 단일 EC2 + Docker Compose는 **OIDC 전체 lifecycle을 가장 작은 surface로 학습**. `KC_HOSTNAME` 미설정 시 `iss` claim mismatch가 단일 host의 **가장 흔한 함정** — browser는 `localhost:8080`, backend는 Docker internal `keycloak:8080` 보면서 JWT issuer가 mismatch → JWT validation 실패. 해결: `KC_HOSTNAME=localhost` + `KC_HTTP_ENABLED=true` 명시. **redirect_uri는 localhost vs 127.0.0.1 한 글자만 달라도 mismatch** → Keycloak client 등록 시 두 URI 모두 등록 또는 사용 일관화. PKCE는 public client에 필수 (RFC 7636) — `code_verifier` 생성 + `code_challenge=SHA256(verifier).base64url`. HTTPS 없이 학습 환경 한정 — prod 진입 시 Caddy 또는 nginx + Let's Encrypt 필수. ## TODO 각 항목 옆에 증거 등급. **현재 모두 `planned`** — 실 구현 후 별도 작업에서 `actually-implemented`/`locally-verified`로 승급. - [ ] docker-compose.yml 작성 (keycloak + postgres + spring + nginx) — `planned` - [ ] Keycloak realm/client 설정 + JSON export — `planned` - [ ] Spring Boot Resource Server (`/api/me` endpoint with `@AuthenticationPrincipal Jwt`) — `planned` - [ ] vanilla JS SPA (login button → PKCE 생성 → callback → token storage → `/api/me` 호출) — `planned` - [ ] iss mismatch issue 재현 + 해결 (`KC_HOSTNAME=localhost` vs `keycloak` 시연) — `planned` - [ ] refresh_token rotation 시연 (Keycloak `Revoke Refresh Token` 옵션 toggle) — `planned` - [ ] HTTPS 없는 환경에서 token 노출 demonstration (Wireshark/curl로 헤더 캡처) — `planned` - [ ] (선택) Caddy reverse proxy로 HTTPS 추가 — `planned` ## 진행 중 메모 작업하며 떠오른 메모. 자유 형식. - **`AXIS_DRIFT` (2026-07-18 `/branch-spec` 확인)** — hub 가 2026-07-14 에 분류 primary 축을 *배치×federation 6패턴* → *인증 아키텍처 4패턴(AP1~AP4)* 로 교정([[raw/project-notes/keycloak-patterns-overview]] §2.3)했으나 본 노트 본문은 여전히 Phase 0 의 "P3A" 프레이밍이다. 매핑은 **P3A → AP1 + cross-cutting 배포=single-EC2 (실 구현 base)**. hub §2.3·§8 이 "실제 rename/re-parent 은 `wiki-doc-author mode=migrate` 로 점진 수행(자동 mv 금지 — wikilink 영향 검토)"라고 명시하므로 본 회차에서는 **본문 재작성 없이 정합 표기만** 추가(제목 blockquote + 본 메모). 물리 `parent_branch:` 는 아직 `feature-keycloak-patterns` 이고, AP 그룹 소속은 hub §8 분해표가 SSOT. - **본 노트는 단일-EC2 실 구현 base — detail 의 owner 는 6 자식이다.** hub F5 가 "실 구현 배포 = single-EC2 1벌" 로 고정하여 본 노트가 그 물리 base 이지만, 실행 단계(docker-compose / realm / SPA / iss / refresh / Spring RS)는 자식 6개가 각각 owner 다(§Cluster). 따라서 본 노트의 `D2`·`D4`·`D5` 는 자식 owner 결정의 *요약*이라 `rules/consistency-contract.md` 의 `RESTATED_FOREIGN_DECISION` 소지가 있다 — 사용자 작성 결정을 덮어쓰지 않고 §구현 가이드 §3 에 **위임 맵**을 세워 포인터를 명시한다. - **`DECISION_DRIFT` 해소 (2026-07-18)** — 과거 parent D4의 manual-first 문구는 **historical/superseded**다. 실제 코딩 순서는 owner [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] D1의 `oidc-client-ts` library-first이며, manual `crypto.subtle` PKCE는 baseline E2E 뒤의 비교 학습 단계다. - **`OWNER_SPLIT` (Spring RS + `aud` 검증) — 2026-07-18** — 본 §Cluster 는 [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] 을 나열하나, hub §8 dup-reconciliation 은 "Spring RS 셋업·`aud` 검증 = [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 가 owner, role-mapping 의 role→RBAC 부분만 deferred authZ" 로 정했다. §구현 가이드 §3 위임 맵은 두 owner 를 분리 지정한다(RS 셋업/aud = audience-validator, 본 노트 Cluster 의 role-mapping 은 role→RBAC deferred). - **repo 부재 (`NO_GROUND_TRUTH` for impl) — 2026-07-18 확인** — `/home/donghyeon/workspace/keycloak-patterns/` 디렉터리가 아직 없다. 따라서 모든 TODO/`구현 계획` 항목은 `planned`(코드로 확인된 `actually-implemented` 아님). 계약 근거는 official docs(Keycloak/Spring/OWASP/RFC)이며, 포트 값 등 배포 상수는 자식 [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D6 이 owner (KC-CONTAINER-C5 가 "포트 값은 공식 raw verbatim 부재" 로 못박음 → 본 노트에서 official 로 단정 금지). - **`TOPOLOGY_DRIFT` (2026-07-18 depth 감사)** — 본 노트 §컴포넌트 다이어그램·§구현 가이드 §1 은 **3-포트 직노출**(nginx=static only, backend/Keycloak 각자 포트)로 토폴로지를 확정하나, hub [[raw/project-notes/keycloak-patterns-overview]] §3-1-5 다이어그램은 nginx `proxy_pass` 리버스프록시(same-origin)를 그린다. 본 노트가 토폴로지 owner(D3 · hub F5)이므로 divergence 를 공개만 하고 3-포트를 학습 기본으로 유지 — cross-origin 귀결(CORS / Web Origins)은 §구현 가이드 §1 + §엣지·실패·의존 에서 종결한다. ## 컴포넌트 다이어그램 ``` EC2 (단일 호스트) ├─ nginx (port 80) → /index.html (vanilla JS SPA static 파일) ├─ Spring Boot (port 8081) → /api/* (Resource Server) └─ Keycloak (port 8080) → /realms//... Browser → EC2:80 → SPA load Browser → EC2:8080 → Keycloak (OIDC redirect: /auth → 로그인 → /callback) Browser → EC2:8081 → Backend (Authorization: Bearer ) Backend → EC2:8080/realms//protocol/openid-connect/certs (JWKS, localhost network) ``` 신뢰 경계: 단일 호스트 내 localhost trust. 외부에서는 EC2 public IP / DNS만 노출. ## 토큰 교환 sequence (P2A와 동일 + localhost 특이점) 1. **SPA: PKCE 생성** — `code_verifier` (랜덤 43–128 char), `code_challenge = BASE64URL(SHA256(code_verifier))`, `code_challenge_method=S256`. verifier는 `sessionStorage` 저장 (단일 auth 라운드트립 수명 — 콜백 직후 폐기하므로 D2/OWASP 의 *장기 토큰* 저장 금지와는 별개다. 단 `sessionStorage` 자체는 XSS 노출면이라 `raw/official-docs/oauth2-pkce-rfc-7636.md` Usage Boundaries 가 별도 플래그). 2. **SPA → Keycloak `/auth` redirect** — query: `client_id`, `redirect_uri`, `response_type=code`, `scope=openid`, `state`, `code_challenge`, `code_challenge_method=S256`. 3. **사용자 로그인** → Keycloak → `redirect_uri` callback with `?code=...&state=...`. 4. **SPA → Keycloak `/token` (POST form)** — `grant_type=authorization_code`, `code`, `redirect_uri`, `client_id`, `code_verifier`. 응답: `access_token` / `refresh_token` / `id_token` / `expires_in`. 5. **SPA → Backend `Authorization: Bearer `**. 6. **Backend → Keycloak JWKS** (`localhost:8080/realms//protocol/openid-connect/certs`) → public key fetch (캐시) → JWT signature verify + `iss` claim 검증. ## 단일 호스트 특이점 ### `KC_HOSTNAME` 함정 (이 패턴의 핵심 학습 포인트) - `iss` claim은 Keycloak이 발급한 JWT 안에 박힘. 예: `iss=http://localhost:8080/realms/keycloak-patterns`. - Browser는 `localhost:8080`으로 Keycloak에 접근, backend도 같은 hostname을 issuer-uri로 등록해야 검증 통과. - Docker Compose에서 backend가 `keycloak:8080`(컨테이너 DNS)로 JWKS를 부르면 issuer mismatch 발생 (token에 박힌 `iss`는 `localhost:8080`인데 backend가 기대하는 issuer가 `keycloak:8080`). - 해결: backend `issuer-uri = http://localhost:8080/realms/...` 로 통일. JWKS도 같은 hostname으로 부르려면 컨테이너에서 호스트 네트워크 공유(`network_mode: host`) 또는 `extra_hosts: [host.docker.internal:host-gateway]` 후 `host.docker.internal` 사용. - 또는 Keycloak `KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true`로 frontchannel/backchannel URL 분리 (Keycloak 24+). ### redirect_uri mismatch - Keycloak client 등록 시 `Valid redirect URIs` 정확히 일치해야 함. - `http://localhost/callback` ≠ `http://127.0.0.1/callback` ≠ `http:///callback`. 셋 다 다른 URI. - 와일드카드 `http://localhost/*` 허용은 학습 환경 한정. prod 금지. ### 기타 - **PKCE는 여전히 필수** — public client (브라우저는 client_secret 보관 불가). - **HTTPS 없으면 token 평문 노출** — `access_token`, `refresh_token`이 HTTP 헤더/응답으로 평문 전송. 학습 환경 한정. - nginx는 단순 static 파일 서빙 (Caddy 또는 nginx + Let's Encrypt로 HTTPS termination 추가 가능). ## 장점 / 단점 ### 장점 - **단일 호스트라 네트워크 디버깅 쉬움.** tcpdump / `docker compose logs`로 한 화면에서 추적. - **docker-compose 한 줄로 환경 reset** (`docker compose down -v && up`). - **학습 곡선 평탄.** k8s / ingress / Traefik 등 부가 인프라 없음. - **localhost trust로 보안 변수 최소화** — 외부 노출은 80/8080/8081 세 포트만. ### 단점 - **운영 환경 모방 X.** 실 운영은 Keycloak / API / static 분리 배치(P1·P2 패턴) — 본 패턴은 학습 전용. - **HTTPS termination 별도 처리 필요.** Caddy reverse proxy를 앞단에 두거나 nginx에 cert 추가. - **단일 EC2 장애 = 전체 다운.** SPOF. - **`KC_HOSTNAME` 설정 잘못 시 디버깅 난이도 급증** (issuer/redirect/JWKS URL 3가지가 얽힘). ## 결정 사항 (decisions) - 2026-05-25: **HTTPS 없이 진행** (학습 환경). prod 진입 시 Caddy 또는 nginx + Let's Encrypt 추가. 이유: cert 발급/갱신 흐름이 본 학습 주제(OIDC)와 무관. - 2026-05-25: pure SPA의 access/refresh token은 **모두 memory-only**로 둔다. reload 시 복원하지 않고 재인증한다. HttpOnly refresh cookie는 TMB/BFF variant이며 본 AP1 baseline이 아니다. - 2026-05-25 (owner 위임): issuer identity와 JWKS network address의 실행 wiring은 [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D6을 따른다. 본 base는 `KC_HOSTNAME` 함정의 배포 축만 소유한다. - 2026-05-25 (historical, superseded): ~~vanilla JS는 manual fetch + `crypto.subtle` 기반 PKCE 구현 우선~~. - 2026-07-18: 실제 코딩 순서는 [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] D1의 **`oidc-client-ts` library-first**다. manual PKCE는 baseline E2E 뒤 비교 학습 단계다. - 2026-05-25: **realm 1개 + client 1개 (`spa-client`, public, Standard Flow + PKCE S256 강제)**. multi-tenant / role mapping은 out of scope. ## 결정-근거 매핑 > 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. P3A 는 학습 환경 단순화 결정 다수 → 일부는 `UNSUPPORTED_DECISION` (공식 근거 없이 학습 우선순위 기반). > > `선택 조건` 열(R2, 2026-07-18 추가): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 `N/A`. > > **Ownership note** — 본 노트는 단일-EC2 배포 base 다. `D1`·`D3` 은 본 branch 고유(HTTPS 경계 · KC_HOSTNAME 배포 축)이고, `D2`·`D4`·`D5` 는 자식 owner 결정의 요약이다(§구현 가이드 §3 위임 맵 · §진행 중 메모 `RESTATED_FOREIGN_DECISION`). 세부는 owner 를 정본으로 본다. | Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk | |---|---|---|---|---|---| | D1 | 학습 환경에서 HTTPS 없이 진행 (Caddy / Let's Encrypt 는 prod 진입 시 추가) | **학습 환경(localhost / 단일 EC2)에서 OIDC lifecycle 자체가 학습 목표**일 때만 HTTP. 외부 노출·prod 진입 시 HTTPS edge termination 필수(`KC-RP-C4`). 또한 frontchannel 이 HTTPS 여야만 브라우저 `crypto.subtle`(S256 계산)이 secure context 로 동작 — `localhost` 예외에만 HTTP 허용(§구현 가이드 §2 `UNSUPPORTED_IMPL_DECISION`) | `UNSUPPORTED_DECISION` — 공식 문서는 prod 에서 HTTPS edge termination 을 권고 (`raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C4`) 이고, 학습 환경에서 HTTP 만으로 OIDC 를 진행하라는 권고는 어느 공식 자료에도 없음 | (학습 우선순위 기반 결정) | HTTP 위에서 토큰이 평문 전송 → 학습 환경 외 노출 시 즉시 노출. `KC-CONTAINER-C3` (`start-dev` insecure defaults) 와 결합 시 prod 절대 금지 | | D2 | pure SPA의 access/refresh token을 모두 memory-only로 보관하고 reload 시 재인증 | **AP1 pure SPA baseline**이면 memory-only. 세션 지속이 요구되면 HttpOnly refresh cookie를 슬쩍 추가하지 않고 AP2(TMB) 또는 AP3(BFF) variant로 전환한다. **owner = [[raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff]] D1**, 구현 consumer = [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] D2 | `raw/official-docs/owasp-html5-storage-xss-spa.md#OWASP-HTML5-C1`, `raw/official-docs/owasp-html5-storage-xss-spa.md#OWASP-HTML5-C2`, `raw/official-docs/owasp-html5-storage-xss-spa.md#OWASP-HTML5-C4` | `official-reference` (owner를 통한 위임) | memory token도 실행 중 XSS에 노출된다. reload UX를 허용할 수 없으면 별도 server-side custody·CSRF 계약을 갖는 variant가 필요 | | D3 | `KC_HOSTNAME`이 정하는 issuer identity와 backend의 JWKS 도달성을 함께 맞춘다 | 실행 profile의 정확한 값과 network mechanism은 [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D6이 owner다. 본 base는 단일-host에서 두 조건이 모두 필요하다는 배포 requirement만 소유 | `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C2`, `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C3`, `raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C1`, `raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C2` | `official-vendor-doc + delegated` | 실제 Docker profile은 owner D6의 401→200 E2E 전까지 `planned` | | D4 | 실제 코딩 순서 = `oidc-client-ts` 우선, manual PKCE는 비교 학습용 별도 단계 | baseline E2E를 먼저 확보할 때 library-first. 내부 알고리즘 비교는 이후 manual 단계. **실행 owner = [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] D1** | owner D1의 `OIDCTS-C2/C3` + `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C2`, `#PKCE-RFC7636-C3` | `delegated official-vendor-doc + official-standard` | manual 단계의 `crypto.subtle` secure-context 동작은 별도 확인 필요 | | D5 | realm 1개 + client 1개 (`spa-client`, public, Standard Flow + PKCE S256 강제) | **단일 패턴 학습**이면 realm 1 / client 1(spa-client public). hub F3 대로 **4 패턴 통합 base 로 확장 시 realm 은 공유 1개 유지, client 는 패턴당 1개**(spa-public / token-mediating-confidential / bff-confidential / edge-proxy)로 분리 — `aud` 로 client 구분. multi-tenant / role mapping 은 out of scope. **owner = [[raw/branch-notes/feature-keycloak-realm-client-export]] D1** (public+PKCE S256) | `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/keycloak-getting-started-docker.md#KC-GSD-C3`, `raw/official-docs/keycloak-getting-started-docker.md#KC-GSD-C4` | `official-standard + official-vendor-doc` | Keycloak Admin UI 에서 PKCE S256 강제 옵션의 정확한 토글명/위치는 공식 quickstart 인용에 없음 — Admin UI 실 확인 필요 | ## 구현 가이드 > 본 branch 는 단일-EC2 **배포 통합 base** (hub F5) — 실행 코드가 아니라 (1) 물리 토폴로지 경계 명세, (2) `KC_HOSTNAME`/`redirect_uri` 배포측 signature 함정, (3) 6 자식 owner 로의 위임 맵이 산출물이다. 아래 sub-section 은 본 branch 고유 결정(D1·D3·D5)에서만 도출하며, 자식이 owner 인 실행 detail 은 재진술하지 않고 포인터로 위임한다(`rules/consistency-contract.md` Reference-Only). 실 구현 등급은 모두 `planned`(repo 부재 — §진행 중 메모 `NO_GROUND_TRUTH`). ### 1. 단일 EC2 토폴로지 경계 — 누가 어디서 무엇을 하는가 > **Trace**: D3 (KC_HOSTNAME 통일 — `KC-HOST-C2`/`KC-HOST-C3`), D5 (realm/client — `PKCE-RFC7636-C1`/`KC-GSD-C3`), hub F5 (single-EC2 배포 base). 본 §가 §컴포넌트 다이어그램을 결정-trace 로 종결한다. > > - **UNSUPPORTED_IMPL_DECISION**: 포트 값(80/8080/8081)·네트워크 메커니즘·컨테이너명은 어느 official 인용도 강제하지 않는다(`KC-CONTAINER-C5` 가 "포트/env 값은 공식 raw verbatim 부재" 로 명시). owner 는 자식 [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D6 — 본 §는 *경계와 노출 정책*만 확정하고 상수는 위임한다. > - **TOPOLOGY / CORS 귀결 (2026-07-18 depth 감사 반영)**: 본 §가 확정한 **3-포트 직노출**(nginx=static only)은 브라우저에 **2개의 cross-origin 레그**를 만든다 — SPA(:80)→Keycloak(:8080) `/token` + SPA(:80)→backend(:8081) `/api`+`Authorization`. 따라서 Keycloak **Web Origins**(`KC-GSD-C4` — "Set Web origins to ...") + backend **Spring CORS** 가 필수다(§엣지·실패·의존 CORS 경로 + §3 위임 맵). ⚠️ **`TOPOLOGY_DRIFT`**: hub §3-1-5 다이어그램은 nginx `proxy_pass` 리버스프록시(same-origin)를 그리나 본 노트는 3-포트 직노출을 그린다(§진행 중 메모 `TOPOLOGY_DRIFT`). 본 노트가 토폴로지 owner(D3 · hub F5)이므로 학습-최소 3-포트를 기본으로 두되, `proxy_pass` 채택 시 `/api` 는 same-origin 화되어 Spring CORS 가 소거된다 — **그래도 SPA→Keycloak `/token` 은 여전히 cross-origin 이라 Web Origins 는 토폴로지와 무관하게 필수**. | 컴포넌트 | 역할 (무엇을 보유 / 수행) | 외부 노출 | 근거 | 등급 | |---|---|---|---|---| | nginx | vanilla JS SPA static 서빙 (public client) | `:80` (외부) | §컴포넌트 다이어그램; 포트 상수는 child [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D6 | `planned` | | Spring Boot (Resource Server) | **토큰 미보유** — 요청마다 JWT를 검증하고 audience 계약을 적용 | `:8081` (외부) | [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1/D6 | `planned` | | Keycloak (Authorization Server) | 토큰 발급 + JWKS publish, `KC_HOSTNAME` 로 issuer hostname 고정 | `:8080` (외부) | `KC-CONTAINER-C1` (KC_HOSTNAME=노출 주소), `KC-HOST-C2`; realm 모델 `KC-GSD-C3` | `planned` | | PostgreSQL | Keycloak realm/user persistence | 내부 only (미노출) | child [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D2 (dev-file 대신 postgres) | `planned` | | trust 경계 | 단일 host localhost trust — 외부는 위 3 포트만, backend↔Keycloak JWKS 는 loopback | — | D3 (localhost 통일); §컴포넌트 다이어그램 신뢰 경계 | `planned` | ### 2. 배포측 signature 함정 — `KC_HOSTNAME`(iss) + `redirect_uri` > **Trace**: D3 (`KC-HOST-C2` hostname 의무·dynamic resolution 차단, `KC-HOST-C3` fraudulent issuer 방어), D5 (`KC-GSD-C4` Valid redirect URIs 설정). 본 §는 **단일 host 배포에서만 발생하는 고유 관심사**이며 §단일 호스트 특이점을 결정-trace 로 종결한다. > > - issuer/JWKS의 실행 profile은 child [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D6이 owner다. 본 §는 함정의 *존재·재현 조건·해결 축*만 확정하고 구체 mechanism을 복제하지 않는다. > - **UNSUPPORTED_IMPL_DECISION**: 브라우저 `crypto.subtle`(S256 계산)은 **secure context** 에서만 노출되어 non-`localhost` HTTP origin 에선 차단된다. trade-off: 학습 환경의 `localhost` 예외에 의존해 HTTP 를 쓰되(D1), 그 외에는 frontchannel = HTTPS 로 둔다 — 본 corpus 에 이 브라우저 제약의 직접 인용 없음(`oauth2-pkce-rfc-7636.md` Usage Boundaries 도 "추가 확인 필요"로만 기록, §Claims To Verify). | 함정 | 발생 (재현 조건) | 기대 동작 / 해결 | 근거 | owner (실행) | |---|---|---|---|---| | **iss mismatch** | browser 는 토큰의 `iss=http://localhost:8080/realms/...` 를 받고, backend 가 `keycloak:8080`(컨테이너 DNS)을 기대 issuer 로 설정 → 모든 요청 `401` | `KC_HOSTNAME=localhost` 통일 + backend `issuer-uri` 동일 값 + loopback 도달 메커니즘 | `KC-HOST-C2`, `KC-HOST-C3`, `SSRS-JWT-C1` | 재현·해결 → [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1/D4/D6 | | **redirect_uri mismatch** | `http://localhost/callback` ≠ `http://127.0.0.1/callback` ≠ `http:///callback` — scheme·host·port·trailing slash 한 글자만 달라도 authorization request 거부 | 등록 URI 와 접근 hostname 1:1 일치 또는 둘 다 등록. wildcard `/*` 는 학습 한정 | `KC-GSD-C4` (Valid redirect URIs 설정 — vendor). exact-match **MUST** 표준(`OA21-C5`)은 본 노트 Sources 밖 → [[raw/branch-notes/feature-keycloak-internal-spa-direct-no-google]] D7 이 owner-absent 로 흡수(Reference-Only) | wildcard 정책 → [[raw/branch-notes/feature-keycloak-realm-client-export]] D2; 실 redirect_uri 값 → [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] D5 (`callback.html`) | | **crypto.subtle secure context** | non-`localhost` HTTP origin 에서 `crypto.subtle.digest('SHA-256', ...)` 차단 → S256 challenge 계산 불가 → PKCE 흐름 실패 | `localhost` 학습만 HTTP 허용, 그 외 frontchannel = HTTPS | `UNSUPPORTED_IMPL_DECISION` (본 corpus 직접 인용 없음, §Claims To Verify) | [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] | ### 3. 결정 위임 맵 (6 자식 owner — Reference-Only) > **Trace**: D2·D4·D5 는 본 base 가 요약만 보유하고 실행 detail 의 owner 는 자식이다(§진행 중 메모 `RESTATED_FOREIGN_DECISION`). `rules/consistency-contract.md` Single-Owner 에 따라 **세부는 owner 를 정본으로** 본다 — 본 표는 포인터 + 1줄 요약만 유지하고 임계값·메커니즘을 재진술하지 않는다. > > - **UNSUPPORTED_IMPL_DECISION**: 없음 — 각 행은 owner 노트의 실존 `D` 을 가리킨다(2026-07-18 확인). | 관심사 | owner (정본) | owner 결정 | 1줄 요약 (본 base 의 인용) | |---|---|---|---| | docker-compose 스택 (keycloak+postgres+nginx+spring, healthcheck, realm auto-import) | [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D1~D6 | `start-dev` + postgres + `depends_on: service_healthy` + `--import-realm` + `KC_HOSTNAME=localhost` + 포트/`.env` secret | §컴포넌트 다이어그램·§구현 계획의 docker-compose 항목 정본 | | realm/client 설정 + JSON export | [[raw/branch-notes/feature-keycloak-realm-client-export]] D1~D5 | public + PKCE S256, redirect wildcard(학습), rotation 값, AT 5분, export+redact commit | 본 base D5 요약의 정본 | | vanilla JS SPA PKCE 코드 | [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] D1~D5 | library-first baseline, manual은 비교 학습 단계 | 본 base D4와 정렬 완료 | | iss 함정 재현·해결 | [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1/D4/D6 | issuer identity/JWKS reachability 실행 profile | 본 base D3·§구현 가이드 §2 의 재현·해결 위임 | | refresh rotation + logout | [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D1/D2/D4/D5 | rotation ON + Max Reuse 0, AT 5분, revoke/logout 분리, rotation flow 시연 | 본 base D2(refresh 저장) 인접 — rotation 정책 정본 | | Spring RS + `aud` validator | [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1/D6 | RS 공통 셋업과 단일 backend audience 검증 | 본 base 백엔드 authN 검증 owner | | role→RBAC (deferred) | [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] D4~D6 | realm/client role을 Spring authority로 변환·강제 | 4패턴 authN E2E 이후 착수 | | CORS 경계 (Keycloak Web Origins + Spring CORS) | Web Origins → [[raw/branch-notes/feature-keycloak-realm-client-export]] · Spring CORS → SecurityFilterChain (RS 셋업 owner = [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] per `OWNER_SPLIT`) | Web Origins 에 SPA origin 등록(`KC-GSD-C4`) + Spring CORS 로 SPA origin whitelist | 3-포트 직노출의 cross-origin 귀결(§구현 가이드 §1 · §엣지·실패·의존) — 본 base 는 경계·owner 만 지정, 값은 owner | ## 엣지·실패·의존 > R4(깊이 게이트) 캡처용. 본 base 는 `planned`(repo 부재) 이나, 단일-EC2 스택을 실제로 세울 때 부딪힐 실패/엣지와 다른 계약 의존을 미리 열거한다. - **실패·엣지 경로**: - **`iss` mismatch (본 배포의 대표 함정)**: browser 는 frontchannel hostname 으로 토큰을 받고 backend 가 컨테이너 DNS(`keycloak:8080`)를 기대 issuer 로 설정하면 전 요청 `401`. 기대 동작: `KC_HOSTNAME=localhost` 통일 + backend `issuer-uri` 동일(D3). 재현·해결과 실행 profile은 [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1/D4/D6에 위임한다. - **`redirect_uri` mismatch → 인증 시작 단계에서 거부**: `iss` 를 맞춰도 그 앞에서 깨지는 경로. `localhost` vs `127.0.0.1` vs public-ip 한 글자만 달라도 authorization request 거부되고 토큰 교환까지 가지 못함. 기대 동작: 등록/접근 hostname 1:1 (§구현 가이드 §2). 근거: `KC-GSD-C4`(vendor) + exact-match MUST 는 [[raw/branch-notes/feature-keycloak-internal-spa-direct-no-google]] D7(`OA21-C5`) 참조. owner: realm wildcard 정책 [[raw/branch-notes/feature-keycloak-realm-client-export]] D2 + 실 redirect_uri [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] D5. - **`crypto.subtle` 차단 (non-localhost HTTP)**: S256 challenge 계산 불가 → PKCE 흐름 실패. 기대 동작: `localhost` 학습만 HTTP, 그 외 HTTPS(D1). `UNSUPPORTED_IMPL_DECISION` — corpus 직접 인용 없음(§Claims To Verify). - **CORS preflight 실패 (본 base 3-포트 직노출의 귀결)**: SPA(:80)→Keycloak(:8080) `/token` 과 SPA(:80)→backend(:8081) `/api`+`Authorization` 은 둘 다 cross-origin → whitelist 없으면 브라우저가 preflight 에서 차단. 기대 동작: Keycloak client **Web Origins** 에 SPA origin 등록(`KC-GSD-C4` — "Set Web origins to ...") + backend **Spring CORS** 로 SPA origin 만 허용. `UNSUPPORTED_IMPL_DECISION`(Spring CORS 측): 본 Sources 에 Spring CORS 직접 인용 없음 — 일반 브라우저 동작 원리. 위임: Web Origins → [[raw/branch-notes/feature-keycloak-realm-client-export]], Spring CORS → SecurityFilterChain owner [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] (RS 셋업, `OWNER_SPLIT`). nginx `proxy_pass` same-origin 화 시 `/api` CORS 는 소거되나 `/token` Web Origins 는 잔존(§구현 가이드 §1 `TOPOLOGY_DRIFT`). - **HTTPS 부재 → 토큰 평문 노출**: `access_token`/`refresh_token` 이 HTTP 헤더/응답으로 평문 전송(D1). 학습 한정, 외부 노출 시 즉시 위험. `KC-CONTAINER-C3`(`start-dev` insecure defaults)와 결합 시 prod 절대 금지. - **refresh token 재사용 탐지**: rotation ON 상태에서 탈취자와 정상 사용자가 같은 refresh 를 쓰면 token family 전체 무효 → 침해 시그널이자 **정상 사용자 강제 로그아웃**(가용성 비용). 기대 동작: 재로그인 유도. 위임: [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D1/D5. - **`aud` 미검증 → cross-client token reuse**: 같은 realm 타 client 토큰이 통과할 수 있다. 기대 동작과 구현 방식은 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1을 따른다. - **Keycloak 미가용**: backend 는 JWKS 를 캐시하므로 이미 발급된 토큰은 캐시 유효 동안 계속 검증되나 신규 로그인은 즉시 차단. 캐시 TTL 수치는 미검증(§Claims To Verify). - **SPOF — 단일 EC2 다운 = 전체 정지**(§장점/단점). 운영급은 P2A cluster + Keycloak HA(hub §5 deferred). - **다른 계약 의존**: - [[raw/project-notes/keycloak-patterns-overview]] §5 고정 결정 **F5**(실 구현 배포 = single-EC2 1벌) — 본 노트가 그 물리 base. F5 가 바뀌어 cluster/edge E2E 가 요구되면 배포 전략 재설계. - [[raw/project-notes/keycloak-patterns-overview]] §2 고정 결정 **F1**(taxonomy AP1~AP4) — 본 노트 = AP1 + 배포=single-EC2. branch 재정의 금지(SSOT 는 hub). - [[raw/project-notes/keycloak-patterns-overview]] §5 고정 결정 **F3**(단일 realm + 패턴당 client 1개) — D5 의 확장 형태. 4 패턴 base 로 쓸 때 client 분리로 `aud` 구분. - [[raw/project-notes/keycloak-patterns-overview]] §5 고정 결정 **F4**(confidential secret = env var, 미커밋) — AP2/AP3 를 이 base 에 얹을 때 `.env`/`KC_*` 주입, realm export 평문 금지. - [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D1~D6 — 본 base 스택의 정본. 포트/network/import 가 바뀌면 §컴포넌트 다이어그램 갱신. - [[raw/branch-notes/feature-keycloak-realm-client-export]] D1~D5 — D5 요약이 의존. - [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] D1~D5 — D4와 정렬된 실행 owner. - [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] D1/D4/D6 — D3 의 재현·해결 위임. - [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D1/D5 — D2 인접. - [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1/D6 — RS 셋업·audience owner. [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] D4~D6 — deferred RBAC owner. - sibling [[raw/branch-notes/feature-keycloak-internal-spa-direct-no-google]] (P2A) D1 — 토큰 교환 흐름 동형(본 §토큰 교환 sequence 가 "P2A와 동일" 선언). AP1 pattern hub 는 P2A. - sibling [[raw/branch-notes/feature-keycloak-single-ec2-google-federation]] (P3B) — Google 변형. 본 no-google base 에 brokering 을 코드 0줄로 얹음. ## 검증해야 할 주장 > 공식 문서는 근거지만, P3A 학습 환경에서의 실제 동작은 별도 검증 필요. | Claim | Why uncertain | How to verify | Status | |---|---|---|---| | `KC_HOSTNAME=localhost` 가 docker-compose 컨테이너 내부에서 의도대로 작동 (token `iss=http://localhost:8080/realms/...`) | 공식 hostname guide 는 `localhost` 사용 권고가 dev/quickstart 한정. 학습 환경에서 host 네트워크 의존성이 컨테이너 격리와 충돌 가능 | `docker compose up -d` 후 access token 발급 → `jwt.io` 또는 `jq` 로 `iss` claim 확인 | `needs-confirmation` | | `network_mode: host` 가 Linux EC2 에서 정상 동작 (Docker Desktop 가정 제약 회피) | Linux 호스트는 host 네트워크 지원, mac/Windows Docker Desktop 은 제약. 학습 환경이 EC2 Linux 인지 로컬 Docker Desktop 인지에 따라 결과 다름 | EC2 ubuntu 에서 `docker compose ps` + `curl http://localhost:8080/realms/keycloak-patterns/.well-known/openid-configuration` 확인 | `planned` | | backend (`spring-boot-starter-oauth2-resource-server`) 가 `issuer-uri=http://localhost:8080/...` 로 startup 시 JWKS discovery 성공 | `SSRS-JWT-C2` 는 4단계 discovery 를 보장하지만 컨테이너 → 호스트 loopback 도달성은 별도 | backend 로그에서 `JwtDecoder` 초기화 메시지 + `/api/me` 호출 결과 확인 | `planned` | | `crypto.subtle.digest('SHA-256', ...)` 가 학습 환경 (`http://localhost`) Secure Context 예외로 사용 가능 | 일반적 HTTP origin 은 Secure Context 아님 → SubtleCrypto 차단. localhost 는 브라우저 vendor 별 예외 처리 | Chrome/Firefox 에서 `app.js` 콘솔에 `await crypto.subtle.digest(...)` 호출 확인 | `needs-confirmation` | | `redirect_uri=http://localhost/callback` 등록 후 `http://127.0.0.1/callback` 으로 callback 시 Keycloak 이 거부 (의도된 mismatch 시연) | `OA21-C5` exact-match 표준은 있으나 Keycloak 의 실제 enforce 동작 (대소문자, trailing slash, host 동등성) 은 별도 | Admin UI 에서 valid redirect URIs 등록 후 hostname 변형 시 `redirect_uri_mismatch` 에러 확인 | `planned` | | Keycloak `Revoke Refresh Token: ON` + `Max Reuse: 0` 토글이 refresh rotation 을 실제로 한 번만 허용 | 공식 인용 부재 (oauth2.1 `OA21-C3` 는 scope/resource binding 만 언급) | refresh token 두 번 연속 사용 → 두 번째 호출에서 4xx 응답 확인 | `planned` | ## 마주친 문제 - (구현 시작 후 추가) `KC_HOSTNAME` 설정 misconfiguration으로 인한 issuer mismatch 예상. - (구현 시작 후 추가) `redirect_uri` 등록 시 `localhost` vs `127.0.0.1` 혼동 예상. ## 구현 계획 - **Repo 위치**: `/home/donghyeon/workspace/keycloak-patterns/` (별도 git repo, LLM Wiki 외부). ⚠️ 2026-07-18 현재 **미존재** — §진행 중 메모 `NO_GROUND_TRUTH`. 아래 전부 `planned`. - **docker-compose.yml**: - `keycloak` (`quay.io/keycloak/keycloak:26.x`, `start-dev`, `KC_HOSTNAME=localhost`, `KC_HTTP_ENABLED=true`, `KC_BOOTSTRAP_ADMIN_USERNAME=admin`) - `postgres` (Keycloak realm persistence, volume mount) - `backend` (Spring Boot 3 + Java 21, `spring-boot-starter-oauth2-resource-server`) - `nginx` (static SPA serve, port 80) - (선택) `caddy` reverse proxy for HTTPS - (실행 detail 정본: [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D1~D6) - **SPA**: `index.html` + `app.js` — [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] D1에 따라 `oidc-client-ts`로 baseline E2E를 먼저 만들고 manual PKCE는 비교 단계에서 수행한다. - **Backend**: - Spring Boot 3 + Java 21 - `application.yml`: `spring.security.oauth2.resourceserver.jwt.issuer-uri: http://localhost:8080/realms/keycloak-patterns` - `/api/me` endpoint with `@AuthenticationPrincipal Jwt` → return `jwt.getClaims()`. - (실행 detail 정본: [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] D1/D6; RBAC는 [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] D4~D6) - **Keycloak realm export JSON**: `keycloak-patterns-realm.json` (realm + client + 테스트 사용자) commit. (실행 detail 정본: [[raw/branch-notes/feature-keycloak-realm-client-export]] D1~D5) ## 관련 - 부모 root: [[raw/branch-notes/feature-keycloak-patterns]] - 동형 (token flow 동일): [[raw/branch-notes/feature-keycloak-internal-spa-direct-no-google]] (P2A Internal SPA + Resource Server, no Google) - 다음 패턴: [[raw/branch-notes/feature-keycloak-single-ec2-google-federation]] (P3B Single EC2 + Google federation) ## 묶음 (자식 sub-sub-branches — P3A 실 구현 6단계) - [[raw/official-docs/keycloak-getting-started-docker]] - [[raw/official-docs/keycloak-hostname-configuration]] - [[raw/official-docs/keycloak-server-containers-docker]] - [[raw/official-docs/oauth2-pkce-rfc-7636]] - [[raw/official-docs/oidc-client-ts-library]] - [[raw/official-docs/spring-security-resource-server-jwt]] - [[raw/branch-notes/feature-keycloak-docker-compose-stack]] - [[raw/branch-notes/feature-keycloak-realm-client-export]] - [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] - [[raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce]] - [[raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch]] - [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] > Sources는 상단 "외부 근거" 섹션. Errors/Interview/Lectures/Blog 는 현재 없음 — P3A 실 구현(Phase 3) 진입 시 errors / interview-prep 등재 예상. ## 관련 일일 노트 ## 완료 후 정리 - PR 링크: (별도 keycloak-patterns repo) - 리뷰 메모: - 머지 결과 / 배포 환경: 단일 EC2(또는 로컬 docker-compose) 시뮬레이션, 로컬 검증까지. - **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출): - `actually-implemented` 항목: (구현 후 채움) - `locally-verified` 항목: (구현 후 채움) - `prod-verified` 항목: (없음, prod 배포 out of scope) - **추출하지 않을 항목** (planned / documented-only / abandoned): 현재 전부 `planned`. 구현 완료된 부분만 wiki/projects/로 승급.