Files
llm-wiki/raw/company-tech-blogs/keycloak-jwt-role-extraction-betweendata.md

11 KiB

title, source_type, url, archive_url, related_branches, related_projects, tags, status, confidence, created, last_reviewed
title source_type url archive_url related_branches related_projects tags status confidence created last_reviewed
Extract roles from access token issued by Keycloak using Spring Security (Between Data / Christian Huff) personal-blog https://betweendata.io/posts/secure-spring-rest-api-using-keycloak/
feature-keycloak-spring-rs-role-mapping
keycloak-patterns
personal-blog
keycloak-patterns
auth
spring-security
keycloak
raw medium 2026-07-18 2026-07-18

Extract roles from access token issued by Keycloak using Spring Security (Between Data / Christian Huff)

Layer: raw/company-tech-blogs/ (분류: 실제 source_typepersonal-blog — 저자 Christian Huff 개인 블로그. 저장소 기존 관행(raw/company-tech-blogs/test-pyramid-vs-trophy-kent-dodds, senior-engineer-competency-mubin-shaikh.md, deliberate-practice-software-developers-redgreencode.md)에 따라 company-tech-blogs/ 디렉토리에 위치하되 frontmatter source_type: personal-blog 유지). 회사 기술 블로그가 아니므로 공식 best practice 로 격상 금지 (CLAUDE.md §5, §11). 아래 모든 Claim 은 engineering-blog 강도 — 개인 저자의 구현 사례일 뿐, Spring/Keycloak 공식 권고가 아니다.

Parent / 활용 branch (필수)

Branch 이 자료가 정당화하는 결정
raw/branch-notes/feature-keycloak-spring-rs-role-mapping Keycloak realm role 을 Spring GrantedAuthority 로 매핑하는 구현 방식 — 손으로 작성한 Converter<Jwt, Collection<GrantedAuthority>> 가 nested realm_access claim 을 읽어 ROLE_ prefix 붙은 authority 로 변환하고, 필요 시 DelegatingJwtGrantedAuthoritiesConverter 로 default scope converter 와 결합하는 접근의 사례(case study) 근거. feature-keycloak-spring-rs-role-mapping D4 (realm role 만 매핑) 의 구현 detail 참고 자료.

출처 / Source

왜 저장했는지 / Why archived

Spring Boot 3 + spring-boot-starter-oauth2-resource-server 환경에서 Keycloak 이 발급한 JWT 의 realm_access/resource_access claim 은 Spring 의 기본 JwtGrantedAuthoritiesConverter 가 자동으로 추출하지 못한다. 이 자료는 그 문제를 커스텀 Converter<Jwt, Collection<GrantedAuthority>> 로 해결한 구체 코드 사례를 담고 있어, feature-keycloak-spring-rs-role-mapping 의 role mapping 구현 detail 을 정당화하는 참고 사례로 보관.

핵심 인용 / Key quotes (verbatim)

[§Extract Roles from Access Token — class declaration] "public class KeycloakJwtRolesConverter implements Converter<Jwt, Collection> {"

[§Extract Roles from Access Token — realm_access claim 이름 정의 + 실제 읽기] "private static final String CLAIM_REALM_ACCESS = "realm_access";" [...] "Map<String, Collection> realmAccess = jwt.getClaim(CLAIM_REALM_ACCESS);"

[§Extract Roles from Access Token — ROLE_ prefix 상수] "public static final String PREFIX_REALM_ROLE = "ROLE_realm_";" [...] "public static final String PREFIX_RESOURCE_ROLE = "ROLE_";"

[§Extract Roles from Access Token — ROLE_ prefix 설명 (본문)] "In the returned authorities the realm roles are prefixed with ROLE_realm_ while the resource roles are prefixed with ROLE_[NAME_OF_THE_RESOURCE]_."

[§Define Access Rules — DelegatingJwtGrantedAuthoritiesConverter 조합] "new DelegatingJwtGrantedAuthoritiesConverter(" [...] "new JwtGrantedAuthoritiesConverter()," [...] "new KeycloakJwtRolesConverter());"

Claims Extracted / 추출된 주장

Claim ID Claim (이 자료가 직접 말하는 것) Evidence quote Strength Applies to Does not prove
KC-ROLE-BD-C1 저자는 Converter<Jwt, Collection<GrantedAuthority>> 를 구현하는 KeycloakJwtRolesConverter 클래스를 작성해, realm_access claim 이름을 상수로 정의하고 jwt.getClaim(CLAIM_REALM_ACCESS) 로 직접 읽는다 "public class KeycloakJwtRolesConverter implements Converter<Jwt, Collection> {" / "private static final String CLAIM_REALM_ACCESS = "realm_access";" / "Map<String, Collection> realmAccess = jwt.getClaim(CLAIM_REALM_ACCESS);" engineering-blog Spring Boot 3 + Spring Security OAuth2 Resource Server 환경에서 Keycloak realm_access (nested claim map) 을 GrantedAuthority 로 변환하는 구현 패턴 이 방식이 Spring 또는 Keycloak 공식 권고 패턴이라는 것은 아님 (원문에 공식 문서 인용 없음). Keycloak 모든 버전에서 realm_access claim 구조가 동일하다는 보증도 아님 — 원문 예시 토큰은 특정 시점(2023-02) Keycloak 버전 기준
KC-ROLE-BD-C2 realm-level role 은 ROLE_realm_ prefix, resource(client)-level role 은 ROLE_[리소스명]_ prefix 를 붙여 SimpleGrantedAuthority 로 변환한다고 명시 "public static final String PREFIX_REALM_ROLE = "ROLE_realm_";" / "public static final String PREFIX_RESOURCE_ROLE = "ROLE_";" / "In the returned authorities the realm roles are prefixed with ROLE_realm_ while the resource roles are prefixed with ROLE_[NAME_OF_THE_RESOURCE]_." engineering-blog Spring Security hasAuthority(...) 매칭을 위한 authority 명명 규칙의 한 예시(개인 저자 관례) ROLE_ prefix 가 Spring Security 의 필수 요구사항이라는 것은 아님 — hasAuthority 는 임의 문자열 매칭이 가능하고, ROLE_ prefix 규칙은 hasRole(...) 사용 시에만 Spring 이 자동으로 붙이는 것과는 다른 맥락(원문은 이 구분을 설명하지 않음)
KC-ROLE-BD-C3 WebSecurityConfiguration.filterChain(...) 에서 DelegatingJwtGrantedAuthoritiesConverter 를 사용해 default JwtGrantedAuthoritiesConverter 와 커스텀 KeycloakJwtRolesConverter 를 함께 등록한다 "new DelegatingJwtGrantedAuthoritiesConverter(" [...] "new JwtGrantedAuthoritiesConverter()," [...] "new KeycloakJwtRolesConverter());" engineering-blog scope 기반 default authority 와 realm/resource role 기반 custom authority를 하나의 authorities 집합으로 합치는 조합 패턴의 사례 이 조합이 모든 프로젝트에 필요하다는 것은 아님 — scope 기반 인가를 병행하지 않는 프로젝트라면 default converter 생략 가능. 원문도 코드 주석 수준("Using the delegating converter multiple converters can be combined")의 설명만 제공하며 DelegatingJwtGrantedAuthoritiesConverter API 계약 자체의 공식 문서화는 아님

참고: 이 raw 는 engineering-blog 강도만 제공 — official 보강 필요

KC-ROLE-BD-C1~C3 는 모두 engineering-blog (개인 저자 사례). branch-note 에서 이를 "공식 best practice" 로 인용하면 안 된다 (CLAUDE.md §5, §11). realm_access 가 default JwtGrantedAuthoritiesConverter 로 자동 매핑되지 않는다는 사실 자체의 공식 근거가 필요하면 raw/official-docs/spring-security-resource-server-jwt (예: 기존 branch-note 인용 SSRS-JWT-C4 — default converter 는 scope/scpSCOPE_ prefix 로 자동 변환) 를 함께 인용해야 official-vendor-doc 급 근거가 된다. 이 raw 단독으로는 D4(realm role 만 매핑) 의 "왜 커스텀 컨버터가 필요한가"에 대한 사례일 뿐, "Spring 이 이렇게 하라고 권고한다"는 근거는 아니다.

Usage Boundaries / 적용 경계

  • 이 자료가 직접 증명하는 것:
    • KC-ROLE-BD-C1: 손으로 작성한 Converter<Jwt, Collection<GrantedAuthority>> 구현이 realm_access claim 을 nested map 으로 읽어올 수 있다는 동작 사례 (저자 GitHub 리포지토리에 테스트 100% 커버리지 존재한다고 원문이 주장 — 코드 자체는 미검증)
    • KC-ROLE-BD-C2: ROLE_realm_ / ROLE_[resource]_ prefix 부여 방식 예시
    • KC-ROLE-BD-C3: DelegatingJwtGrantedAuthoritiesConverter 로 default + custom converter 를 합치는 코드 구조 예시
  • 이 자료가 증명하지 않는 것:
    • 이 구현이 Spring Security 또는 Keycloak 의 공식 권장 패턴이라는 명제 — 원문은 개인 저자의 "minimally invasive" 선택 설명일 뿐, RFC/공식 문서 인용 없음
    • realm_access.roles 매핑이 모든 Keycloak 버전·모든 client 설정에서 동일하게 동작한다는 명제 — 예시 토큰은 특정 realm/client 설정(backend realm, rest-api client) 기준
    • ROLE_ prefix 없이 hasAuthority/hasRole 을 섞어 쓸 때의 Spring Security 내부 동작 차이에 대한 설명 — 원문 미포함
  • 내 프로젝트에 적용하려면 추가 확인이 필요한 것:
    • feature-keycloak-spring-rs-role-mapping 이 실제로 resource_access (client-level role) 까지 매핑할지, 아니면 D4 결정대로 realm_access 만 매핑할지 — 이 raw 의 KeycloakJwtRolesConverter 는 두 claim 을 모두 처리하므로 branch 결정과 범위가 다름(branch 는 realm role만, 이 raw 는 realm+resource 모두)에 주의
    • 로컬 Keycloak 인스턴스에서 발급한 access token 의 realm_access.roles 실제 JSON 구조가 이 raw 의 예시 토큰과 일치하는지 확인
    • DelegatingJwtGrantedAuthoritiesConverter 조합이 feature-keycloak-spring-rs-role-mapping 의 범위(§구현 가이드)에 실제로 필요한지 — branch 는 @PreAuthorize 대신 SecurityFilterChain matcher 를 우선하기로 결정했으므로 (D5), 이 raw 의 .requestMatchers(...).hasAuthority(...) 패턴과의 정합 재검토 필요

메모 / Notes

검증되지 않은 내 해석은 wiki source-summary 단계에서만.

  • 원문은 realm-level role 과 resource(client)-level role 을 모두 매핑하는 구현(KeycloakJwtRolesConverter)을 제시하지만, feature-keycloak-spring-rs-role-mapping 의 D4 는 "realm role만 매핑 (resource_access 무시)"로 범위를 좁혔다 — 이 raw 를 인용할 때 resource_access 부분은 branch 범위 밖임을 명시해야 함 (OUT_OF_BRANCH_SCOPE 유사 주의).
  • 원문 저자는 Keycloak 기본 설정(mapper 미변경)을 유지하는 쪽을 "minimally invasive" 라고 표현 — 이는 branch 의 "Keycloak mapper 커스터마이징 대신 Spring 쪽 컨버터로 흡수" 방향과 같은 트레이드오프 축으로 보인다(해석, 미검증).
  • 저자는 GitHub 코드 링크(ChristianHuff-DEV/secure-spring-rest-api-using-keycloak)와 100% 테스트 커버리지를 주장하나, 이 raw 는 블로그 본문만 발췌·검증했고 GitHub 코드 자체는 self-grep 대상에 포함하지 않음 — 실제 사용 시 코드 diff 재확인 필요.