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 |
|
1 | feature-keycloak-internal-spa-direct-no-google | feature-keycloak-patterns |
|
|
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와 신뢰 경계 수준까지 명확히 설명할 수 있게 한다.
핵심 질문:
- 왜 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 에 의해 문서 전용)
범위
포함 범위
- 패턴 정의 (본 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.mdSingle-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-uriwiring, 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 토큰 흐름까지만. issmismatch 함정의 재현·해결 절차 — 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.ymlsnippet — 등급: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 정책: "일괄 자동 수정 금지,/syncfix-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 함정으로 갖는다.
컴포넌트 다이어그램
┌──────────────────┐
│ 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)
- PKCE 준비 (SPA):
code_verifier: 43~128 byte random string (RFC 7636 §4.1).code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))(S256 method).state,noncerandom 값 생성 (CSRF/replay 방지).
- 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 - 사용자 로그인 → Keycloak이 redirect with
?code=<auth_code>&state=.... - 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. - 토큰 저장 (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.
- API 호출 (SPA → Backend):
GET /api/... Authorization: Bearer <access_token> - JWT 검증 (Backend, Spring Security Resource Server):
- JWKS endpoint(
/realms/<realm>/protocol/openid-connect/certs)에서 public key fetch + 캐싱. - signature 검증 (
kid매칭). issclaim =https://<keycloak>/realms/<realm>일치.audclaim에 backend client id 포함 (customOAuth2TokenValidator추가 필요).exp/nbf시간 검증 (기본 clock skew 60s).
- JWKS endpoint(
- 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만 확인.audienceclaim은 반드시 customOAuth2TokenValidator로 추가 검증 (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: ONRefresh 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+aud4종. 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-onlypattern hub (D8) — 실행 코드가 아니라 패턴 경계의 사전 명세 + 자식 owner 로의 위임 맵이 산출물이다. 아래 sub-section 은 본 branch 고유 결정(D1·D4·D7·D8)에서만 도출하며, 자식이 owner 인 detail 은 재진술하지 않고 포인터로 위임한다(rules/consistency-contract.mdReference-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-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-C1frontchannel/backchannel 분리 지원,KC-HOST-C2hostname 의무·dynamic resolution 차단,KC-HOST-C3fraudulent 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,hostnameoption 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.mdUsage 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_uriexact-match 는 어느 형제 branch 도 실제로 소유하지 않음을 전수 확인했다. 후보와 배제 근거:
- raw/branch-notes/feature-keycloak-google-redirect-uri-policy D1 — Google 측 Authorized redirect URI(
/broker/google/endpoint) 소관. Google 이 검증하는 URI 이지 Keycloak 이 SPA 에게 검증하는 URI 가 아니라 다른 계약.- 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 의 "부차 함정"(
localhostvs127.0.0.1mismatch)으로도 지목하고 있어 실 구현 시 동일 원리로 재현된다. 후속(본 branch 밖,/sync대상): hub 의STALE_OWNER포인터를 D7 로 갱신. (실 구현 branch raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce D5 에→ 완료 2026-07-18OA21-C5근거 역참조 연결/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) 이나, 패턴을 실제로 세울 때 부딪힐 실패/엣지와 다른 계약 의존을 미리 열거한다.
-
실패·엣지 경로:
issmismatch (본 배치의 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) Dockerextra_hosts이고, (F) Springissuer-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 이 외부 도달 불가하면
/authredirect 단계에서 실패. 기대 동작: ingress 경로 확보(D7). 본 노트 다이어그램이 이 경로를 생략하고 있음(INGRESS_UNDERSPECIFIED). redirect_urimismatch → 인증 시작 단계에서 거부:iss를 맞춰도 그 앞에서 깨지는 경로다. ingress hostname 이 Keycloak client 에 등록된 URI 와 정확히 일치하지 않으면(scheme·port·trailing slash·localhostvs127.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)
- 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
- 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/ 문서는 생성 불가.