34 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-spring-rs-audience-validator (Spring Security Resource Server + audience validator) | branch-note | raw | BR-KEYCLOAK-PATTERNS-OVERVIEW-004 | project-work-item | keycloak-patterns-overview | WI-KEYCLOAK-PATTERNS-OVERVIEW-004 |
|
|
1 | feature-keycloak-spring-rs-audience-validator |
|
|
2026-05-25 | in-progress | 792d7570a248139de64c5bc1fd29f79218e3eea3388db85a4d18e6175344e3b3 |
branch: feature-keycloak-spring-rs-audience-validator — Spring Security Resource Server + audience validator
Layer:
raw/branch-notes/— raw/project-notes/keycloak-patterns-overview의WI-KEYCLOAK-PATTERNS-OVERVIEW-004직접 branch. 목적: Spring Security Resource Server 기본 JWT validator가 검증하는 항목과 별도 활성화가 필요한aud를 분리한다. 단일 audience는 Bootaudiencesproperty를 baseline으로, 복합 조건은 customOAuth2TokenValidator<Jwt>로 구현한다.status_label:in-progress|review|merged|abandoned
정합 노트 (2026-07-14 감사): 본 노트 = AP1 의
aud검증 + Spring RS 공통 셋업 owner (hub Branch 분해 Tier-2feature-keycloak-spring-rs-audience-validator). 형제 raw/branch-notes/feature-keycloak-spring-rs-role-mapping 와 RS 셋업 내용이 겹치는데, 그쪽의 role → 권한(RBAC) 부분은 §5 deferred authZ 트랙으로 분리됨. 구현 시 RS 공통 코드·aud검증은 본 노트가 owner.
부모 (필수)
raw/project-notes/keycloak-patterns-overview
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: foreign audience token 수용 실패를 재현하고 validator 적용 후 401을 검증한다
상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
DEC-KEYCLOAK-PATTERNS-OVERVIEW-SPA-CLIENT-001@1 |
AP1은 public client와 Authorization Code + PKCE를 사용한다 | AP1 Resource Server의 JWT audience 검증에 적용한다 | raw/project-notes/keycloak-patterns-overview |
DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1 |
done-bar는 E2E success와 signature security failure 재현·해결 evidence다 | foreign audience token 실패 재현과 401 evidence를 완료 조건으로 사용한다 | 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 |
|---|
없음.
목표
Spring Security 6.x Resource Server는 spring-boot-starter-oauth2-resource-server + issuer-uri 설정만으로 자동으로 JWT signature / iss / exp / nbf를 검증한다. 그러나 aud claim 검증은 기본 활성화 안 됨. 같은 Keycloak realm 내 다른 client용으로 발급된 토큰이 본 backend로 흘러들어도 통과될 위험이 있다 (cross-client token reuse).
핵심 질문:
- Spring Security 기본
JwtDecoder가 검증하는 것 vs 검증하지 않는 것? audclaim은 왜 별도로 검증해야 하는가? (cross-client / cross-resource-server token reuse 차단)- 다중 issuer 환경(multi-realm)에서 어떻게 처리하는가?
- JWKS cache 정책 (TTL, refresh, key rotation) 기본값은?
본 sub-sub-branch는 의존성 → yml 설정 → 단일 audience property baseline → 복합 조건용 validator 비교까지 정리한다. 프로젝트 expected audience의 단일 심볼은 backend-client-id다.
- 이슈: (학습 노트, 이슈 없음)
- PR: (구현 없음)
범위
포함 범위
- Spring Security Resource Server 공통 셋업의 owner — 의존성(
spring-boot-starter-oauth2-resource-server) +application.yml의issuer-uri+JwtDecoder빈 커스터마이즈(D6). 형제 raw/branch-notes/feature-keycloak-spring-rs-role-mapping 가 이 셋업을 fold-in 으로 위임(정합 노트 2026-07-14). audclaim 검증(본 branch 고유 핵심, D1) — Spring 기본이 검증하지 않는 audience 를 Bootaudiencesproperty 또는 customOAuth2TokenValidator<Jwt>로 추가해 cross-client / cross-resource-server token reuse 를 차단.- Keycloak 발급 측
aud주입 요건(D4) — SPA client 의 client scope 에 Audience mapper 를 등록해 backend client_id 가aud에 포함되도록. 발급 설정은 검증 성립의 선행 조건. - 검증 항목 매트릭스(§구현 가이드 §3) — signature/
iss/exp/nbf는issuer-uri로 자동,aud/azp/scope는 수동 추가 대상임을 분리.
제외 범위
의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.
- role → 권한(RBAC) 매핑(
realm_access.roles→@PreAuthorize) — 형제 raw/branch-notes/feature-keycloak-spring-rs-role-mapping 의 deferred authZ 트랙 소관(D3). 본 노트는 authN 토큰 검증까지만. - 다중 issuer / multi-realm(
JwtIssuerAuthenticationManagerResolver) — 단일 realm 학습 범위 밖(D2,UNSUPPORTED_DECISION). - prod JWKS custom cache(Caffeine 등) 튜닝 — 기본 cache 동작만 문서화(D5). 커스텀 cache 는 범위 밖.
- 실 구현 / 배포 — 실제 코드는 P3A raw/branch-notes/feature-keycloak-single-ec2-no-google + 형제 role-mapping 이 별도 keycloak-patterns repo(현재 미생성)에서 수행. 본 노트는
documented-only설계·계약 층. - PKCE 발급 흐름 / 토큰 저장 위치 / refresh rotation — 각 형제 sub-sub-branch owner 소관(raw/branch-notes/feature-keycloak-pkce-flow-stages · raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff · raw/branch-notes/feature-keycloak-refresh-token-rotation). 본 노트는 발급된 토큰의 검증 측만.
근거 (필수, 최소 1개+)
- raw/official-docs/spring-security-resource-server-jwt — Spring Security Resource Server JWT 검증 reference
- raw/official-docs/oauth-v2-1-draft-ietf — OAuth 2.1 (audience binding 권고)
- raw/official-docs/keycloak-securing-apps-overview-official — Keycloak audience mapper
TODO
각 항목 옆에 증거 등급 표기: actually-implemented | locally-verified | prod-verified | documented-only | planned | needs-confirmation
- 의존성 정리 — 등급:
documented-onlyorg.springframework.boot:spring-boot-starter-oauth2-resource-server- (선택)
org.springframework.security:spring-security-oauth2-jose— 자동 포함 - Java 21 / Spring Boot 3.x / Spring Security 6.x 가정
application.ymlissuer-uri 설정 — 등급:documented-onlyspring: security: oauth2: resourceserver: jwt: issuer-uri: https://<keycloak-host>/realms/<realm>- 효과: Keycloak
/.well-known/openid-configuration자동 fetch → JWKS endpoint 발견 → JwtDecoder 자동 구성 - 자동 검증: signature +
iss == issuer-uri+exp+nbf(clock skew 60s)
- 효과: Keycloak
- JwtDecoder 빈 (복합 조건일 때만 커스터마이즈) — 등급:
documented-only- 기본 빈에
OAuth2TokenValidator<Jwt>체인 추가 NimbusJwtDecoder.withIssuerLocation(issuerUri).build()사용JwtValidators.createDefaultWithIssuer(issuerUri)+ custom validator를DelegatingOAuth2TokenValidator로 결합
- 기본 빈에
- 단일 audience baseline —
spring.security.oauth2.resourceserver.jwt.audiences: backend-client-id— 등급:documented-only - 복합 audience validator 비교 학습 sketch — 등급:
documented-onlypublic class AudienceValidator implements OAuth2TokenValidator<Jwt> { private final String expectedAudience; public OAuth2TokenValidatorResult validate(Jwt jwt) { if (jwt.getAudience() != null && jwt.getAudience().contains(expectedAudience)) { return OAuth2TokenValidatorResult.success(); } return OAuth2TokenValidatorResult.failure( new OAuth2Error("invalid_token", "Missing required audience", null)); } }- Keycloak
audclaim 주의: 기본은 client_id가aud로 들어가지 않을 수 있음 → Keycloak Client Scope의 Audience mapper를 추가해야 backend client_id가aud에 포함됨
- Keycloak
- 다중 issuer 환경 처리 — 등급:
documented-only- 단일 backend가 multi-tenant인 경우:
JwtIssuerAuthenticationManagerResolver.fromTrustedIssuers(...)사용 - 각 issuer마다 JwtDecoder 별도 캐싱
- 본 P2A 학습 범위는 단일 realm 기준 — multi-realm은 SSOT §8 자신 없는 부분에 있음
- 단일 backend가 multi-tenant인 경우:
- JWKS cache 정책 — 등급:
documented-only- 기본: 5분 cache (Spring Security
NimbusJwtDecoder기본Cache-Control따름) - Keycloak 키 회전 시
kidmismatch 발생 → 자동 refresh (Spring Security가 unknown kid 시 JWKS 재fetch) - prod에서는
JwkSetUriJwtDecoderBuilder.cache(Cache)로 custom cache(Caffeine 등) 권장 — 학습 범위 외
- 기본: 5분 cache (Spring Security
- 검증 항목 매트릭스 — 등급:
documented-onlyclaim Spring 기본 추가 필요 signature ✅ (JWKS) — iss✅ — exp/nbf✅ (skew 60s) — aud❌ ✅ 단일= audiences: backend-client-id, 복합=custom validatorazp(authorized party)❌ (선택) 단일 client 강제 시 추가 scope❌ (decoder 단계 아님) @PreAuthorize("hasAuthority('SCOPE_xxx')")
진행 중 메모
작업하며 떠오른 메모. 자유 형식.
- Keycloak의
audclaim 동작은 직관과 다름 — backend client는 보통bearer-only타입인데, SPA client가 backend의 client_id를aud에 포함시키려면 SPA client scope에 Audience mapper를 추가해야 함. 안 그러면aud는account(realm 내장 client)만 들어감. DelegatingOAuth2TokenValidator로 default + audience를 묶는 패턴은 Spring Security 공식 reference의 audience validation 섹션 코드 그대로 적용 가능./branch-spec채움 (2026-07-18) — pre-template 노트를 템플릿 정합으로 보강:선택 조건(R2) 열 · In/Out scope ·## 구현 가이드(§1 RS 셋업 · §2 audience validator · §3 검증 매트릭스) ·## 엣지·실패·의존추가. NO_GROUND_TRUTH — 본 branch 는 ca-tmpl 이 아니라 keycloak-patterns 학습 프로젝트이고 실 구현 repo(/home/donghyeon/workspace/keycloak-patterns/)가 아직 없어 전 항목documented-only/planned유지(코드 grep 불가). depth 게이트 = Ready(Blocking 0). 자가 보강: silent-bypass 엣지(validator 미합성 →aud무검사 통과) + Audience mapper 프로비저닝 owner 포인터 추가. 미해소 Should-fix(연구 opt-in 필요): ① D4 의 "Keycloak 은 client_id 를aud에 자동 미포함"의 official verbatim 부재 → Keycloak Server Admin Guide §Client Scopes/Audience mapper 재발췌 필요, ② D7 의 access-token audience binding 근거가 refresh-token(OA21-C3)과 mismatch → OAuth 2.1 access-token best-practice § 재발췌, ③ §2 custom validator wiring 을 Spring Reference §Configuring Validation 재발췌로 supported 승격. 셋 다documented-only를 벗어나 문서 승급 전 종결 대상.
결정 사항 (decisions)
추후 면접/회고에서 "왜 이렇게 했나" 답할 근거. 대안과 함께 기록.
- 2026-05-25 (정합 2026-07-18): backend는 **
iss+ signature +exp+aud**를 검증한다. audience 검증 자체는 필수지만 단일 값backend-client-id는 Bootaudiencesproperty가 baseline이고, custom validator는 다중 audience·azp같은 복합 조건의 비교/확장 경로다. - 2026-05-25: 다중 issuer는 학습 범위 외. 단일 realm 기준 정리.
- 2026-05-25:
JwtAuthenticationConverter로realm_access.roles를 Spring authorities로 매핑하는 것은 본 sub-sub-branch 범위에서 제외 (인가 영역).
결정-근거 매핑
본 branch Sources: Spring Security RS JWT (
official-vendor-doc), OAuth 2.1 draft (official-standard), Keycloak securing apps overview (official-vendor-doc). 본 mapping 은 세 source 의 직접 인용 가능한 claim 만 사용.
선택 조건열(R2, 2026-07-18/branch-spec추가): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면N/A.Ownership note — 본 노트는 형제 raw/branch-notes/feature-keycloak-internal-spa-direct-no-google (부모 hub, AP1) 의 D3(백엔드 4종 검증) 요약의 정본 owner 이고, 형제 raw/branch-notes/feature-keycloak-spring-rs-role-mapping 의 RS 공통 셋업 +
aud검증 을 fold-in 으로 흡수한다(정합 노트 2026-07-14). role→권한(RBAC)만 그쪽 deferred 트랙에 남는다.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | backend는 iss + signature + exp + aud를 검증한다. expected audience는 backend-client-id 하나이며 단일 값은 Boot audiences property가 baseline |
JWT를 직접 신뢰하는 Resource Server(AP1)면 audience 검증은 필수. 다중 audience/조건부 검증이면 custom OAuth2TokenValidator로 확장 |
raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C1, #SSRS-JWT-C2, #SSRS-JWT-C6, raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3 |
official-vendor-doc + official-standard |
backend-client-id가 실제 Audience mapper와 token aud에 들어가는지는 realm export/token E2E 전까지 needs-confirmation |
| D2 | 다중 issuer 는 학습 범위 외, 단일 realm 기준 정리 | 단일 realm 학습 범위면 단일 issuer-uri. 한 백엔드가 여러 realm(multi-tenant) 토큰을 받으면 JwtIssuerAuthenticationManagerResolver.fromTrustedIssuers(...) 로 확장 — 본 학습 범위 밖(문헌으로 미조사, UNSUPPORTED_DECISION 유지) |
UNSUPPORTED_DECISION (학습 범위 결정 — 외부 자료가 직접 뒷받침하지 않음. SSRS-JWT 의 JwtIssuerAuthenticationManagerResolver 언급은 본 branch raw 발췌에 포함되지 않음) |
UNSUPPORTED_DECISION | 면접/포트폴리오에 multi-tenant Resource Server 경험 주장 금지. documented-only 등급 엄격 유지 |
| D3 | JwtAuthenticationConverter 로 realm_access.roles 를 Spring authorities 로 매핑하는 것은 본 sub-sub-branch 범위 제외 |
authN(누구인가)까지가 본 노트. authZ(realm role → 권한) 가 필요하면 형제 raw/branch-notes/feature-keycloak-spring-rs-role-mapping 의 deferred authZ 트랙. Spring default 는 scope/scp 만 매핑하므로 realm role 은 어느 쪽에서 하든 converter customize 필요 |
raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C4 (scope/scp → SCOPE_ prefix default 동작) — Does not prove: "Keycloak realm role 이 default 로 자동 매핑된다는 뜻은 아님 — realm_access.roles 는 JwtAuthenticationConverter customize 필요" |
official-vendor-doc |
본 결정은 범위 분리 — Spring default 가 Keycloak realm role 을 자동 매핑하지 않는다 는 SSRS-JWT-C4 의 Does-not-prove 와 정합. 형제 branch feature-keycloak-spring-rs-role-mapping 에서 다룸 |
| D4 | Keycloak 의 aud claim 에 backend client_id 가 자동 포함되지 않음 → SPA client 의 client scope 에 Audience mapper 등록 필수 |
backend client_id 로 aud 를 검증하려는 모든 경우(= D1 성립의 선행 조건). Keycloak 기본은 client_id 를 aud 에 안 넣으므로(대신 aud=account) 발급 측 Audience mapper 없이는 audience 검증이 항상 실패 → 대안 없음(발급 설정이 선행). audience 검증을 포기하면 D1 자체가 무너짐 |
raw/official-docs/keycloak-securing-apps-overview-official.md#KC-SECAPP-C1 (Keycloak 통합 일반 원칙), (보조) raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C6 (aud 검증 메커니즘) |
official-vendor-doc |
"Keycloak 기본 동작은 client_id 를 자동으로 aud 에 포함하지 않음" 의 직접 verbatim 은 본 branch Sources 의 KC-SECAPP-C1 |
| D5 | JWKS cache 기본 정책 (5분, kid mismatch 시 자동 refresh) | 학습/기본 환경이면 Spring 기본 cache 동작에 위임. prod 에서 회전 빈도·가용성 SLA 가 빡세면 custom cache(Caffeine 등)로 교체 — 본 노트 범위 밖. UNSUPPORTED: 기본값 수치(5분)·refetch 동작 자체가 미검증(§Claims To Verify) |
UNSUPPORTED_DECISION (본 branch Source 중 SSRS-JWT-C1~C6 어디에도 "5분 cache" 또는 "kid mismatch refresh" 의 verbatim quote 없음. Spring Security NimbusJwtDecoder cache 동작은 별도 § 또는 source code 정독 필요) |
UNSUPPORTED_DECISION | 본 branch 본문 진술 ("기본 5분 cache", "unknown kid 시 JWKS 재fetch") 은 운영 경험/추정. 정확한 verbatim source 추출 필요 |
| D6 | application.yml 의 issuer-uri 한 줄로 OIDC discovery + JWKS 자동 fetch + iss/exp/nbf 자동 검증 (clock skew 60s) |
authorization server 가 OIDC discovery 지원(Keycloak O)이면 issuer-uri 한 줄. discovery 미지원 또는 RS 가 독립 부팅(startup 시 AS ping 회피)을 요구하면 jwk-set-uri 병기(SSRS-JWT-C5). clock skew 는 기본값에 의존하되 시계 편차가 큰 환경이면 JwtTimestampValidator 로 override |
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 |
"clock skew 60s" 의 직접 verbatim quote 는 본 branch Source raw 발췌 (SSRS-JWT-C1~C6) 에 미포함 — Spring Security JwtTimestampValidator default 값. 별도 정확 확인 필요 |
| D7 | OAuth 2.1 draft 가 audience binding 을 권고한다는 진술 | N/A — 표준 근거 진술(결정 분기 아님). access token audience binding 의 직접 quote 는 부분 corroborate(인용된 OA21-C3 는 refresh token binding) → §Claims To Verify 로 access-token 측 § 재발췌 필요 |
raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3 (refresh token MUST be bound to scope + resource servers) |
official-standard |
OA21-C3 는 refresh token binding 만 직접 다룸. access token audience binding 의 직접 권고 quote 는 OAuth 2.1 draft 의 다른 § (e.g., §4.x 또는 §5.x token best practice) 별도 정독 필요. 현재 D7 은 부분 corroborate |
구현 가이드
본 branch 는
documented-only— 실 구현은 P3A(별도 keycloak-patterns repo, 현재 미생성)로 위임(§완료 후 정리). 따라서 산출물은 실행 코드가 아니라 다음 구현자가 되묻지 않고 코드를 쓸 수 있는 사전 명세다. 아래 sub-section 은 본 branch 의 결정(D1·D4·D6)에서만 도출하며, 형제 owner detail 은 포인터로 위임한다(rules/consistency-contract.mdReference-Only). 모든 실 구현 등급은planned.
1. Spring Security Resource Server 셋업 (의존성 → yml → JwtDecoder 빈)
Trace: D6(
SSRS-JWT-C1issuer-uri→iss self-configure,SSRS-JWT-C24단계 deterministic discovery) + D1(SSRS-JWT-C6audiences 로 aud 검증). 형제 raw/branch-notes/feature-keycloak-spring-rs-role-mapping 가 이 셋업을 fold-in 으로 소비한다 — 본 §가 그 공통 셋업의 owner.
- UNSUPPORTED_IMPL_DECISION: clock skew 값(D6 본문의 "60s")·JWKS cache TTL(D5 의 "5분")은 인용 claim 이 보증하지 않는 Spring 기본값 — 아래 표에
needs-confirmation으로 표기하고 §Claims To Verify 로 검증. 임의로 "60s/5분"을 명세에 각인하지 않는다.
| 단계 | 무엇 | 메커니즘 (되묻지 않을 명세) | 근거 | 등급 |
|---|---|---|---|---|
| 의존성 | Resource Server 활성화 | org.springframework.boot:spring-boot-starter-oauth2-resource-server (Java 21 / Spring Boot 3.x / Spring Security 6.x). spring-security-oauth2-jose 는 전이 포함 |
SSRS-JWT-C1 (RS 가 issuer-uri 로 self-configure) |
planned |
| yml | discovery + 자동 검증 | spring.security.oauth2.resourceserver.jwt.issuer-uri: https://<kc-host>/realms/<realm> → /.well-known/openid-configuration fetch → JWKS 발견 → signature+iss+exp+nbf 자동 |
SSRS-JWT-C1, SSRS-JWT-C2 |
planned |
| yml(대안) | AS ping 없이 독립 부팅 | discovery 미지원/독립 부팅이면 jwk-set-uri 병기 — 이때도 issuer-uri 는 유지(iss 검증 위해). startup 시 AS ping 안 함 |
SSRS-JWT-C5 |
planned |
| 단일 audience | property로 audience 활성화 | spring.security.oauth2.resourceserver.jwt.audiences: backend-client-id |
D1 / SSRS-JWT-C6 |
planned |
| JwtDecoder 빈 | 복합 조건일 때만 custom validator 합성 | NimbusJwtDecoder의 기본 validator를 보존하고 custom audience/azp 조건을 추가 |
아래 §2 UNSUPPORTED_IMPL_DECISION |
planned |
| 시간 검증 | clock skew | 기본값 사용. 편차 큰 환경만 JwtTimestampValidator(Duration) override |
D6 Open Risk — 기본값 수치 needs-confirmation |
planned |
2. Audience validator (본 branch 고유 핵심 — D1)
Trace: D1(
SSRS-JWT-C6— Bootaudiencesproperty 가aud검증을 활성화,iss또는aud불일치 시 실패) + D4(KC-SECAPP-C1— 발급 측 설정 선행). 검증 코드 sketch 는 §TODO 의AudienceValidator implements OAuth2TokenValidator<Jwt>참조(중복 재작성 안 함).
- UNSUPPORTED_IMPL_DECISION (핵심 갭): Boot
audiencesproperty vs customOAuth2TokenValidator선택.SSRS-JWT-C6은 property 방식만 보증하고, custom validator +DelegatingOAuth2TokenValidator로 default 와 합성하는 정확한 wiring 은 인용 범위 밖(그 claim 의 Does-not-prove 열이 "별도 §Configuring Validation 페이지 참조"로 명시). trade-off: 단일 audience 면 property 한 줄이 단순·안전(권장), 다중 audience / 조건부(azp 병행 등) 면 custom validator 가 필요 — 본 branch 는 학습상 custom 코드 sketch 를 보유하되 property 를 baseline 근거로 둔다. ▶ 후속(권장 next research): Spring Security Reference "Configuring Validation / Validating an Audience" 절을wiki-source-summarizer로 재발췌해SSRS-JWT-C7추가 → 이 갭을 supported 로 승격.
| 검증 방식 | 언제 | wiring | 근거 상태 |
|---|---|---|---|
Boot audiences property |
단일 audience, Boot 3.x (프로젝트 baseline) | spring.security.oauth2.resourceserver.jwt.audiences: backend-client-id 한 줄 |
supported (SSRS-JWT-C6) |
custom OAuth2TokenValidator<Jwt> |
다중 audience / 조건부 로직 | §TODO sketch(jwt.getAudience().contains(expectedAudience)) + §1 의 DelegatingOAuth2TokenValidator 합성 |
UNSUPPORTED_IMPL_DECISION (wiring 인용 범위 밖 — 위 참조) + §Claims To Verify |
| 발급 측 선행(Keycloak) | 두 방식 공통 | SPA client → Client Scopes → Audience mapper(Included Client Audience = backend-client-id) |
D4 (KC-SECAPP-C1 보조 — runtime 확인 필요) |
3. 검증 항목 매트릭스 (무엇이 자동 / 무엇이 수동)
Trace: D1 + D6. §TODO 의 "검증 항목 매트릭스" 를 명세로 승격 — 각 claim 이
issuer-uri로 자동인지 수동 추가인지 확정.
| claim | Spring 기본 (issuer-uri) |
추가 필요 | 근거 |
|---|---|---|---|
| signature (JWKS) | ✅ 자동 | — | SSRS-JWT-C2 (JWKS 로 public key 검증 strategy) |
iss |
✅ 자동 | — | SSRS-JWT-C1, SSRS-JWT-C2 |
exp / nbf |
✅ 자동 (clock skew 기본값 — needs-confirmation) |
— | SSRS-JWT-C2 + D6 Open Risk |
aud |
❌ | ✅ §2 audience validator | SSRS-JWT-C6 |
azp (authorized party) |
❌ | (선택) 단일 client 강제 시 | UNSUPPORTED_IMPL_DECISION — 본 branch Sources 에 azp 직접 인용 없음(§Claims To Verify). trade-off: OIDC Core §2 근거 필요, 현재 매트릭스 통찰만 |
scope |
❌ (decoder 단계 아님) | @PreAuthorize("hasAuthority('SCOPE_x')") |
SSRS-JWT-C4 (scope→SCOPE_ prefix) |
| JWKS cache / kid 회전 | (기본 cache — needs-confirmation) |
prod 는 custom cache | D5 UNSUPPORTED_DECISION (§Claims To Verify) |
엣지·실패·의존
R4(깊이 게이트) 캡처용. 본 branch 는
documented-only이나, audience 검증을 실제로 세울 때 부딪힐 실패/엣지와 다른 계약 의존을 미리 열거한다.
-
실패·엣지 경로:
aud미검증 → cross-client token reuse: Spring 기본 validator 는aud를 보지 않으므로(SSRS-JWT-C6은 property 를 켜야 검증됨) 같은 realm 의 다른 client 토큰이 본 백엔드에서 통과한다 — 본 branch 존재 이유. 기대 동작: audience validator 로 401(D1).- validator 미합성 →
audsilent bypass (음성 테스트 필수, 가장 위험): §구현 가이드 §2 의DelegatingOAuth2TokenValidator합성을 틀리면 — audience validator 빈만 만들고JwtDecoder.setJwtValidator(...)등록을 누락하거나, default validator 를 덮어써 audience 를 미합성하는 경우 —aud가 조용히 무검사로 통과한다. 위 "mapper 부재"의 loud 401 과 정반대로 아무 에러 없이 cross-client 토큰이 통과해 "검증이 있다"는 착각을 남기는 가장 위험한 실패다. 기대 동작: 잘못된aud(다른 client) 토큰이 반드시 401 임을 음성 테스트로 못박는다(형제 raw/branch-notes/feature-keycloak-spring-rs-role-mapping §TODO 의 "잘못된 aud 토큰 → 401" 로컬 검증과 동일). Trace: §2UNSUPPORTED_IMPL_DECISION(wiring 인용 범위 밖) + D1. - 발급 측 mapper 부재 → 정상 토큰도 거부: Keycloak 이
aud에 backend client_id 를 안 넣으면(기본aud=account) audience validator 가 정상 사용자 토큰도 401. 함정: 검증 코드가 맞아도 발급 설정이 빠지면 전 사용자 로그인 실패. 기대 동작: SPA client scope 에 Audience mapper 선행(D4). Audience mapper 의 실제 realm/client-scope 프로비저닝은 raw/branch-notes/feature-keycloak-realm-client-export(realm export) 소관 — 본 노트는 요건(D4)만 owner. - JWKS 미가용 / kid 회전 mismatch: Keycloak 키 회전 시 백엔드 캐시된 key 로 signature 검증 실패 → 새 kid 로 JWKS 재fetch 기대. 그러나 cache TTL·자동 refetch 동작은 미검증(D5
UNSUPPORTED). 기대 동작: 재fetch 로 자동 복구(가정), §Claims To Verify 로 확인. - clock skew 경계: iat/exp 경계에서 발급자·검증자 시계 편차로 갓 발급된 토큰이
nbf/exp에 걸릴 수 있음. 기대 동작: 기본 skew 허용 — 단 기본값 수치 미검증(D6). 편차 큰 환경은JwtTimestampValidatoroverride. - 다중 audience 토큰:
aud가 배열이고 backend client_id 를 포함하면 통과(contains). Boot property 방식과 customcontains방식의 동작 차이(단일 vs 부분집합)는 §Claims To Verify 로 대조. - issuer-uri startup unreachable: 백엔드 기동 시 Keycloak 미가용이면 discovery 실패로 startup 실패(
SSRS-JWT-C2는 첫 요청 시 discovery). 완화:jwk-set-uri병기로 AS ping 회피(SSRS-JWT-C5) 또는 docker-composedepends_on: healthy(형제 role-mapping §마주친 문제가 지적).UNSUPPORTED_IMPL_DECISION— 완화책 선택 기준은 배포 branch 소관.
-
다른 계약 의존:
- raw/branch-notes/feature-keycloak-internal-spa-direct-no-google (부모 hub, AP1) D3 — 본 노트가 그 요약의 정본 owner. hub 의 §신뢰 경계 체크리스트 "
aud검증"·§토큰 교환 sequence step 7 이 본 노트 결정을 consume. 본 노트 D1/D4 가 바뀌면 hub 갱신 필요(비차단 전파). - raw/branch-notes/feature-keycloak-spring-rs-role-mapping D4~D6 — 본 D1/D6 위에 얹히는 deferred RBAC consumer. RS/audience detail은 그 문서가 소유하지 않는다.
- raw/project-notes/keycloak-patterns-overview §5 고정 결정 F3(단일 realm + 패턴당 client 1개) — D1 의 audience 검증이 client 를 구분하는 전제. client 를 분리하지 않으면
aud로 client 를 구별할 수 없어 audience 검증이 무의미. - raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch D1 — 본 노트 D6 의
iss검증이 성립하려면 KeycloakKC_HOSTNAME고정으로 tokeniss가 백엔드issuer-uri와 byte-level 일치해야 함. issuer 불일치 함정의 재현·해결은 그 branch 소관(single-EC2 맥락, 동일 원리). - raw/branch-notes/feature-keycloak-pkce-flow-stages · raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff — 같은 AP1 그룹. 토큰이 어떻게 발급·저장되는지 전제이며 본 노트는 발급된 토큰의 검증 측만. 계약 의존은 약함(경계 구분 유지).
- raw/branch-notes/feature-keycloak-internal-spa-direct-no-google (부모 hub, AP1) D3 — 본 노트가 그 요약의 정본 owner. hub 의 §신뢰 경계 체크리스트 "
검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
Boot audiences: backend-client-id baseline이 정상 token을 허용하고 wrong-audience token을 거부하는지 |
방식 선택은 D1에서 종결됐지만 runtime repo가 없음 | 정상 aud→200, 다른 client aud→401을 E2E 확인; 복합 조건이 생길 때만 custom 방식 비교 |
planned |
Keycloak SPA client 의 Client Scopes → Audience mapper 등록이 backend client_id 를 aud claim 에 정확히 포함시키는지 |
본 branch 본문 D4 - Keycloak 운영 경험 진술, verbatim Source 부재 | Keycloak admin console 에서 audience mapper 추가 → SPA 로그인 후 token decode 로 aud claim 에 backend client_id 포함 확인 |
planned |
clock skew 60s 가 Spring Security 6.x default 인지 + 어떤 property 로 override 가능한지 |
본 branch 본문 진술 — D6 의 verbatim Source 부재 | Spring Security JwtTimestampValidator source code 또는 JwtValidators factory method 의 default 값 확인 + reference doc 정확 quote 추출 |
needs-confirmation |
Spring Security NimbusJwtDecoder JWKS cache 기본 TTL 이 5분인지 + kid mismatch 시 자동 JWKS refetch 동작 |
D5 가 UNSUPPORTED — verbatim Source 부재 | reference doc §Customizing the JwtDecoder 또는 NimbusJwtDecoder.cache(...) API doc 확인 |
needs-confirmation |
azp (authorized party) claim 검증 추가가 single-client 강제 시 실제 필요한지 + Keycloak 이 azp 를 발급 token 에 포함시키는지 |
본 branch 본문 검증 항목 매트릭스의 "선택" 항목 — Source 부재 | OIDC Core §2 ID token 의 azp 정의 정독 + Keycloak 발급 token 의 azp claim 실제 존재 확인 |
planned |
JwtIssuerAuthenticationManagerResolver.fromTrustedIssuers(...) 가 multi-realm 시나리오에서 각 issuer 마다 JwtDecoder 를 별도 캐싱하는지 |
본 branch 본문 진술 — 본 branch raw 발췌 (SSRS-JWT-C1~C6) 에 미포함 | Spring Security reference doc 의 multi-tenancy 섹션 정독 후 새 Claim 인용 추가 | planned |
Keycloak realm role (realm_access.roles / resource_access.<client>.roles) 가 JwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(...) 로 추출 가능한지 |
SSRS-JWT-C4 의 Does-not-prove 가 customize 필요 명시 — 정확한 claim name 미확정 |
형제 branch feature-keycloak-spring-rs-role-mapping 의 결정과 결합, 실제 token 의 realm_access.roles 구조 확인 후 converter 동작 검증 |
planned |
| OAuth 2.1 draft 의 access token audience binding 직접 권고 quote 가 어느 § 에 위치 | D7 부분 corroborate — OA21-C3 는 refresh token 만 | OAuth 2.1 draft 전체 정독 → access token audience binding § 확인 후 Claim ID 추가 (OA21-C7 등) | planned |
마주친 문제
- 이슈 1: Keycloak에서 SPA client가 받는 토큰의
audclaim에 backend client_id가 안 들어감.- 원인: Keycloak 기본 동작은 client_id를 자동으로
aud에 포함하지 않음. SPA client의 client scope에 audience mapper를 등록해야 함. - 시도: (구현 없음)
- 해결: SPA client → Client Scopes → Add → Audience mapper (Included Client Audience = backend-client-id) —
documented-only
- 원인: Keycloak 기본 동작은 client_id를 자동으로
묶음
- raw/official-docs/keycloak-securing-apps-overview-official
- raw/official-docs/spring-security-resource-server-jwt
본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
오류 기록 (이 sub-sub-branch 작업 중 발생)
- (없음 — 현재 documented-only 단계)
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (없음 — Phase 3 실 구현 단계에 누적)
관련 일일 노트
완료 후 정리
머지/종료 시점에 채움.
/ingest가 이 섹션을 기준으로 wiki/projects/에 추출.
- PR 링크: (미구현 — 문서까지만)
- 리뷰 메모:
- 머지 결과 / 배포 환경: 없음 (P2A는
documented-only범위) - wiki 추출 대상: 현 단계 없음. 추후 P3A 구현 후
wiki/concepts/spring-security-jwt-validation.md로 합성 검토 가능. - 추출하지 않을 항목: P2A 구현 없음.
documented-only유지.