Search code examples
javaspring-securitykeycloakspring-webfluxspring-security-oauth2

Webflux and keycloak using for method security level @Preauthorize custom claim in jwt instead of default scope


As per title I am setting up webflux config like this

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class WebSecurityConfig {
String jwkSetUri = "http://localhost:8080/auth/realms/demo/protocol/openid-connect/certs";
@Bean 
public ReactiveJwtDecoder jwtDecoder() {

return 
ReactiveJwtDecoders.fromIssuerLocation("http://localhost:8080/auth/realms/demo");
}
 @Bean

public SecurityWebFilterChain configure(ServerHttpSecurity http) {

     http.authorizeExchange()

           
            .oauth2Login()

            .and()
          .
            ...
             .oauth2ResourceServer()
             .jwt()

    .jwkSetUri(jwkSetUri)

            // .jwtDecoder(jwtDecoder())

             .jwtAuthenticationConverter(grantedAuthoritiesExtractor());
            return http.build();
}

@Bean

Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {

    GrantedAuthoritiesExtractor extractor = new GrantedAuthoritiesExtractor();

    return new ReactiveJwtAuthenticationConverterAdapter(extractor);

}
static class GrantedAuthoritiesExtractor

       extends  JwtAuthenticationConverter {


    public Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {

        Collection<?> authorities = (Collection<?>)

                jwt.getClaims().getOrDefault("roles", Collections.emptyList());


        return authorities.stream()

                .map(Object::toString)

                .map(SimpleGrantedAuthority::new)

                .collect(Collectors.toList());
    }
}

with the following application.yml

security:
   oauth2:

     client:
       provider:
         keycloak-local:
           issuer-uri: http://localhost:8080/auth/realms/demo
       registration:
         keycloak-local:
           client-id: some-id
           client-secret: ...

     resourceserver:
       jwt:
     
         issuer-uri: http://localhost:8080/auth/realms/demo
         jwk-set-uri: http://localhost:8080/auth/realms/demo/protocol/openid- 
         connect/certs

 

the token format is like that

"realm_access": {
"roles": [
 "offline_access",
  "uma_authorization"
  ]
   },
 "resource_access": {
 "someclient": {
  "roles": [
    "uma_protection"
   ]
  },
  "account": {
    "roles": [
    "manage-account",
    "manage-account-links",
    "view-profile"
  ]
   }
   },
   "scope": "profile  email ",
   "organizationId": "605b74813bd72c65a24a1704",
   "accountId": "605b74813bd72c65a24a1709",
   "email_verified": false,
   "roles": [
   "ROLE_ADMIN"    
     ],
   "preferred_username": "someUsername",
   "userId": "605b74813bd72c65a24a1706",
   "email": "email.com"
    }`


    
  

and of course the "roles" array is the one that I would like ideally to access(is the role that I define in my DB).

After that setup it seems that nothing has changed . So in my controller I tried to use the @Preauthorize annotation to access to some method but it keeps working with the scope claim.

@PreAuthorize("hasAuthority('SCOPE_email')")

Does anyone knows what I am missing? Please note that I refer to official documentation but also to this tutorial. for webflux.

Thanks


Solution

  • By default, Spring Security converts the items in the scope or scp claim and uses the SCOPE_ prefix. If your application is a pure OAuth2 Resource Server, you can change both conventions by defining a custom ReactiveJwtAuthenticationConverter bean.

    For example, to export authorities from a roles scope and using the `` prefix (since ROLE_ is already part of your role name), you can define the following bean. Spring Security will pick it up automatically, you don't need to reference it from your SecurityWebFilterChain configuration.

    @Bean
    public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
        var jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
    
        var jwtAuthenticationConverter = new ReactiveJwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
                new ReactiveJwtGrantedAuthoritiesConverterAdapter(jwtGrantedAuthoritiesConverter));
    
        return jwtAuthenticationConverter;
    }
    

    Since your application is also configured as an OAuth2 Client (with oauth2Login()), then you need a different approach. You can either define a GrantedAuthoritiesMapper or an OAuth2UserService. Examples for both options can be found in the Spring Security documentation.