Files
llm-wiki/raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md
T

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
DEC-KEYCLOAK-PATTERNS-OVERVIEW-SPA-CLIENT-001@1
DEC-KEYCLOAK-PATTERNS-OVERVIEW-ACCEPTANCE-001@1
WI-KEYCLOAK-PATTERNS-OVERVIEW-003
1 feature-keycloak-spring-rs-audience-validator
keycloak-patterns
branch
keycloak-patterns
p2a
spring-security
resource-server
jwt
audience
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-overviewWI-KEYCLOAK-PATTERNS-OVERVIEW-004 직접 branch. 목적: Spring Security Resource Server 기본 JWT validator가 검증하는 항목과 별도 활성화가 필요한 aud를 분리한다. 단일 audience는 Boot audiences property를 baseline으로, 복합 조건은 custom OAuth2TokenValidator<Jwt>로 구현한다. status_label: in-progress | review | merged | abandoned

정합 노트 (2026-07-14 감사): 본 노트 = AP1 의 aud 검증 + Spring RS 공통 셋업 owner (hub Branch 분해 Tier-2 feature-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 검증하지 않는 것?
  • aud claim은 왜 별도로 검증해야 하는가? (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.ymlissuer-uri + JwtDecoder 빈 커스터마이즈(D6). 형제 raw/branch-notes/feature-keycloak-spring-rs-role-mapping 가 이 셋업을 fold-in 으로 위임(정합 노트 2026-07-14).
  • aud claim 검증(본 branch 고유 핵심, D1) — Spring 기본이 검증하지 않는 audience 를 Boot audiences property 또는 custom OAuth2TokenValidator<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/nbfissuer-uri 로 자동, aud/azp/scope 는 수동 추가 대상임을 분리.

제외 범위

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

근거 (필수, 최소 1개+)

TODO

각 항목 옆에 증거 등급 표기: actually-implemented | locally-verified | prod-verified | documented-only | planned | needs-confirmation

  • 의존성 정리 — 등급: documented-only
    • org.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.yml issuer-uri 설정 — 등급: documented-only
    spring:
      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)
  • JwtDecoder 빈 (복합 조건일 때만 커스터마이즈) — 등급: documented-only
    • 기본 빈에 OAuth2TokenValidator<Jwt> 체인 추가
    • NimbusJwtDecoder.withIssuerLocation(issuerUri).build() 사용
    • JwtValidators.createDefaultWithIssuer(issuerUri) + custom validator를 DelegatingOAuth2TokenValidator로 결합
  • 단일 audience baselinespring.security.oauth2.resourceserver.jwt.audiences: backend-client-id — 등급: documented-only
  • 복합 audience validator 비교 학습 sketch — 등급: documented-only
    public 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 aud claim 주의: 기본은 client_id가 aud로 들어가지 않을 수 있음 → Keycloak Client Scope의 Audience mapper를 추가해야 backend client_id가 aud에 포함됨
  • 다중 issuer 환경 처리 — 등급: documented-only
    • 단일 backend가 multi-tenant인 경우: JwtIssuerAuthenticationManagerResolver.fromTrustedIssuers(...) 사용
    • 각 issuer마다 JwtDecoder 별도 캐싱
    • 본 P2A 학습 범위는 단일 realm 기준 — multi-realm은 SSOT §8 자신 없는 부분에 있음
  • JWKS cache 정책 — 등급: documented-only
    • 기본: 5분 cache (Spring Security NimbusJwtDecoder 기본 Cache-Control 따름)
    • Keycloak 키 회전 시 kid mismatch 발생 → 자동 refresh (Spring Security가 unknown kid 시 JWKS 재fetch)
    • prod에서는 JwkSetUriJwtDecoderBuilder.cache(Cache) 로 custom cache(Caffeine 등) 권장 — 학습 범위 외
  • 검증 항목 매트릭스 — 등급: documented-only
    claim Spring 기본 추가 필요
    signature (JWKS)
    iss
    exp / nbf (skew 60s)
    aud 단일=audiences: backend-client-id, 복합=custom validator
    azp (authorized party) (선택) 단일 client 강제 시 추가
    scope (decoder 단계 아님) @PreAuthorize("hasAuthority('SCOPE_xxx')")

진행 중 메모

작업하며 떠오른 메모. 자유 형식.

  • Keycloak의 aud claim 동작은 직관과 다름 — backend client는 보통 bearer-only 타입인데, SPA client가 backend의 client_id를 aud에 포함시키려면 SPA client scope에 Audience mapper를 추가해야 함. 안 그러면 audaccount(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는 Boot audiences property가 baseline이고, custom validator는 다중 audience·azp 같은 복합 조건의 비교/확장 경로다.
  • 2026-05-25: 다중 issuer는 학습 범위 외. 단일 realm 기준 정리.
  • 2026-05-25: JwtAuthenticationConverterrealm_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-mappingRS 공통 셋업 + 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 JwtAuthenticationConverterrealm_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.rolesJwtAuthenticationConverter 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-C1C3 / SSRS-JWT-C1C6 어디에도 없음 — Keycloak Server Administration Guide §Client Scopes / Audience mapper 정독으로 별도 corroborate 필요. 현재 본 branch 본문 운영 경험만
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.ymlissuer-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.md Reference-Only). 모든 실 구현 등급은 planned.

1. Spring Security Resource Server 셋업 (의존성 → yml → JwtDecoder 빈)

Trace: D6(SSRS-JWT-C1 issuer-uri→iss self-configure, SSRS-JWT-C2 4단계 deterministic discovery) + D1(SSRS-JWT-C6 audiences 로 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 — Boot audiences property 가 aud 검증을 활성화, iss 또는 aud 불일치 시 실패) + D4(KC-SECAPP-C1 — 발급 측 설정 선행). 검증 코드 sketch 는 §TODO 의 AudienceValidator implements OAuth2TokenValidator<Jwt> 참조(중복 재작성 안 함).

  • UNSUPPORTED_IMPL_DECISION (핵심 갭): Boot audiences property vs custom OAuth2TokenValidator 선택. SSRS-JWT-C6property 방식만 보증하고, 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 미합성 → aud silent 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: §2 UNSUPPORTED_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). 편차 큰 환경은 JwtTimestampValidator override.
    • 다중 audience 토큰: aud 가 배열이고 backend client_id 를 포함하면 통과(contains). Boot property 방식과 custom contains 방식의 동작 차이(단일 vs 부분집합)는 §Claims To Verify 로 대조.
    • issuer-uri startup unreachable: 백엔드 기동 시 Keycloak 미가용이면 discovery 실패로 startup 실패(SSRS-JWT-C2 는 첫 요청 시 discovery). 완화: jwk-set-uri 병기로 AS ping 회피(SSRS-JWT-C5) 또는 docker-compose depends_on: healthy(형제 role-mapping §마주친 문제가 지적). UNSUPPORTED_IMPL_DECISION — 완화책 선택 기준은 배포 branch 소관.
  • 다른 계약 의존:

검증해야 할 주장

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가 받는 토큰의 aud claim에 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

묶음

본 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 유지.