Files
llm-wiki/raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md
T

60 KiB

title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, parent_branch, related_projects, tags, created, target_merge, status_label, contract_packet_sha256
title source_type status id kind project work_item inherits refines overrides depends_on contract_packet branch parent_branch related_projects tags created target_merge status_label contract_packet_sha256
branch / feature-keycloak-internal-spa-direct-no-google (P2A Internal SPA + Resource Server, no Google) branch-note raw BR-KEYCLOAK-CHILD-D594F009 branch-child keycloak-patterns-overview WI-KEYCLOAK-PATTERNS-OVERVIEW-020
DEC-KEYCLOAK-PATTERNS-OVERVIEW-AUTH-TAXONOMY-001@1
1 feature-keycloak-internal-spa-direct-no-google feature-keycloak-patterns
keycloak-patterns
branch
keycloak-patterns
auth
oauth2
oidc
2026-05-25 in-progress 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).

부모 (필수)

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 변형이다 Google 없는 SPA Direct 패턴을 AP1과 internal deployment 변형으로 분류한다 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

없음.

목표

SPA가 Keycloak에 직접 OIDC + PKCE로 토큰을 받고, 백엔드는 JWT 검증만 하는 패턴을 토큰 교환 sequence와 신뢰 경계 수준까지 명확히 설명할 수 있게 한다.

핵심 질문:

OAuth 2.1 draft가 implicit flow를 제거하고 PKCE를 모든 authorization code flow에 의무화한 이유를 SPA 관점에서 정리.

  • 이슈: (학습 노트, 이슈 없음)
  • PR: (구현 없음 — D8 에 의해 문서 전용)

범위

포함 범위

  • 패턴 정의 (본 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 준수).

제외 범위

의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.

근거 (필수, 최소 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 권고 패턴.

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.mdRESTATED_FOREIGN_DECISION 소지가 있다 — owner 가 진화하면 낡은 복제본이 된다. 본 회차에서는 사용자 작성 결정을 덮어쓰지 않고(retro 정책: "일괄 자동 수정 금지, /sync fix-plan 으로 점진 수거") §구현 가이드 §3 에 위임 맵을 세워 포인터를 명시했다.
  • INGRESS_UNDERSPECIFIED — 본 노트 §컴포넌트 다이어그램은 Browser (SPA) ─── OIDC ───► Keycloak (cluster-internal) 로 그려져 있으나, 브라우저는 cluster-internal 서비스에 직접 도달할 수 없다. Authorization Code 흐름은 브라우저가 /authredirect 되고 /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 함정으로 갖는다.

컴포넌트 다이어그램

                                            ┌──────────────────┐
                                            │  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):
    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):
    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):
  6. API 호출 (SPA → Backend):
    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 DELEGATEDraw/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-C1PKCE 사용 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)까지만 보장한다. issKC_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-C1C2 (Keycloak 보안 모델), KC-HOST-C2 (hostname 고정) ※ 2026-07-17 축소 — 원래 C1C3 로 인용했으나 KC-SECAPP-C3verbatim 원문 부재 자체가 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)까지만 보장한다. issKC_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_uriowner 부재 → 본 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 를 소비시킬 수 있음). nonceid_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 계열 인용으로 보강 필요.
  • 다른 계약 의존:

검증해야 할 주장

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.ymlaudiences 만 설정 → 잘못된 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 고정을 주장 issKC_HOSTNAME 결합 자체는 single-EC2 로 검증 가능 — P3A raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch D1 이 이미 소유(KC_HOSTNAME=localhostiss=http://localhost:8080/realms/... 관찰). ② 그러나 frontchannel/backchannel 분리 시 /.well-known/openid-configurationissuer 가 어느 쪽으로 표시되는지는 그 분리 토폴로지를 세워야만 확인 가능 — 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)

Sources는 하단 "외부 근거" 섹션. Errors/Interview/Lectures/Blog 는 현재 없음.

관련 sub-branch

관련 일일 노트

완료 후 정리

  • PR 링크: (미구현 — 문서까지만)
  • 머지 결과 / 배포 환경: 없음
  • wiki 추출 대상: 현 단계 없음. P2A는 documented-only 범위 — wiki/concepts/로의 추출은 다른 패턴들과 함께 비교 매트릭스가 완성된 뒤에만.
  • 추출하지 않을 항목: P2A는 본 branch에서 구현 안 함. actually-implemented/locally-verified 등급의 자체 wiki/projects/ 문서는 생성 불가.