I have decoded Jwt token like that. I added User Realm Role Token Mapper in Client Scope using Keyclock server. Thus we can access that claim directly from jwt.
{
"exp": 1707597391,
"iat": 1707597091,
"jti": "505e8da0-1035-49fc-be74-1c59ab15ac03",
"iss": "http://localhost:8181/realms/spring-boot-microservices-realm",
"aud": "account",
"sub": "bcb69d29-b6a4-4c74-96f5-4e89e3f741a1",
"typ": "Bearer",
"azp": "spring-cloud-client",
"session_state": "f36ff533-9403-4b15-9099-d1679e4afd13",
"acr": "1",
"allowed-origins": [
"/*"
],
"realm_access": {
"roles": [
"offline_access",
"ADMIN",
"uma_authorization",
"default-roles-spring-boot-microservices-realm"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "email profile",
"sid": "f36ff533-9403-4b15-9099-d1679e4afd13",
"email_verified": false,
"roles": [
"offline_access",
"ADMIN",
"uma_authorization",
"default-roles-spring-boot-microservices-realm"
],
"preferred_username": "admin"
}
Thats here my Security Config file
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) throws Exception{
http
.authorizeExchange(exchange -> exchange
.pathMatchers("/api/product/**").permitAll()
.pathMatchers("/api/user/**").hasRole("ADMIN")
.anyExchange().authenticated())
.oauth2ResourceServer((oauth2) -> oauth2
.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public JwtAuthenticationConverter con(){
JwtAuthenticationConverter c = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter cv = new JwtGrantedAuthoritiesConverter();
cv.setAuthorityPrefix(""); // Default "SCOPE_"
cv.setAuthoritiesClaimName("roles"); //Default "scope" or "scp"
c.setJwtGrantedAuthoritiesConverter(cv);
return c;
}
}
I can obtain roles on my Jwt token and i use JwtAuthenticationConverter however still i can not access role based path it throws me 403 Forbidden exception. I didn't understands what is the problem exactly.
You have 3 claims containing roles in your sample token, with the following JSON path (in the solutions below, I expose how to extract all):
$.roles
$.realm_access.roles
$.resource_access.account.roles
In your conf, you are using hasRole("ADMIN")
(which expects a ROLE_ADMIN
authority), but without adding a prefix to a Keycloak role which is ADMIN
. This won't work. Two options:
hasRole("ADMIN")
to hasAuthority("ADMIN")
hasRole("ADMIN")
but add a ROLE_
prefix to Keycloak rolesJwtAuthenticationConverter
is for servlet applications. It won't work in a SecurityWebFilterChain
. Instead, configure a ReactiveJwtAuthenticationConverter
as http.oauth2ResourceServer((oauth2) -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(reactiveJwtAuthenticationConverter)))
In addition to spring-boot-starter-oauth2-resource-server
(which you should already have), add a dependency to this Spring Boot starter I maintain:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>7.5.3</version>
</dependency>
Instead of spring.security.oauth2.resourceserver.jwt.*
:
issuer: http://localhost:8181/realms/spring-boot-microservices-realm
com:
c4-soft:
springaddons:
oidc:
ops:
- iss: ${issuer}
authorities:
- path: $.roles
prefix: ROLE_
- path: $.realm_access.roles
prefix: ROLE_
- path: $.resource_access.*.roles
prefix: ROLE_
resourceserver:
permit-all:
- /api/product/**
@Configuration
public class SecurityConfig {
@Bean
ResourceServerAuthorizeExchangeSpecPostProcessor accessControl() {
return spec -> {
return spec.pathMatchers("/api/user/**").hasRole("ADMIN");
};
}
}
spring-boot-starter-oauth2-resource-server
Change your security configuration to the following:
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http, Converter<Jwt, Mono<AbstractAuthenticationToken>> authenticationConverter) throws Exception {
http.authorizeExchange(exchange -> exchange
.pathMatchers("/api/product/**").permitAll()
.pathMatchers("/api/user/**").hasRole("ADMIN")
.anyExchange().authenticated())
.oauth2ResourceServer((oauth2) -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(authenticationConverter)));
http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance());
http.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
ReactiveJwtAuthenticationConverter jwtAuthenticationConverter(Converter<Jwt, Flux<GrantedAuthority>> authoritiesConverter) {
final var jwtAuthenticationConverter = new ReactiveJwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return jwtAuthenticationConverter;
}
static interface ReactiveAuthoritiesConverter extends Converter<Jwt, Flux<GrantedAuthority>> {}
@SuppressWarnings("unchecked")
@Bean
ReactiveAuthoritiesConverter authoritiesConverter() {
return jwt -> {
final List<String> allRoles = new ArrayList<>();
final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
allRoles.addAll((List<String>) realmAccess.getOrDefault("roles", List.of()));
final var resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access", Map.of());
for(final var clientId : resourceAccess.keySet()) {
final var clientAccess = (Map<String, Object>) resourceAccess.getOrDefault(clientId, Map.of());
allRoles.addAll((List<String>) clientAccess.getOrDefault("roles", List.of()));
}
allRoles.addAll((List<String>) jwt.getClaims().getOrDefault("roles", List.of()));
return Flux.fromStream(allRoles.stream().map(r -> "ROLE_%s".formatted(r)).map(SimpleGrantedAuthority::new));
};
}
}
The configuration is almost the same on a servlet resource server when using "my" starter, but you have to change quite a few interfaces when using only spring-boot-starter-oauth2-resource-server
.
Also, if the number of resource servers behind the gateway increases, you'll have to duplicate quite some configuration code (duplicated code always is a problem on the long term).
Apparently, your are configuring on the gateway the access control to downstream resource servers. For many reasons, I would not do that and set it on each resource server instead:
hasRole('ADMIN') or #user.id == authentication.name
which is impossible if you don't know the implementation of a User
resourceIf your frontend is a single-page or mobile application, have a look at the OAuth2 BFF pattern and consider configuring the gateway as an OAuth2 client with oauth2Login()
(and the TokenRelay
filter when routing a request to a resource server). Otherwise, the gateway would probably be better without security at all.