Search code examples
spring-bootspring-securityoauth-2.0

SpringBoot3 filter requests based on a specific claims


I'm using springboot v3.2.3 with the following dependencies

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>

Here my config

@Configuration
public class SecurityConfig {


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("swagger-ui/**", "/v3/api-docs/**").permitAll()
                        .anyRequest().authenticated())
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

        return http.build();
    }
}

I have this in applications.properties

spring.security.oauth2.resourceserver.jwt.issuer-uri=<issuer-uri>
spring.security.oauth2.resourceserver.jwt.audiences=<app>

The jwt token verification works correctly (by allowing only the specified audience and the application uses the right issuer public certificate ). However, I want to filter based on other claims, like roles. what's the cleanest way to do so ?


Solution

  • In my case the claim names are slightly different ! the scope is under app_scopes claim and the role is under the app_roles claim, that's why SpringSecurity didn't map correctly theses claims with the authorities !

    enter image description here

    Hence in order to configure the roles/scopes we have to use a custom jwt converter ( which converts the jwt token into an authentication object )

    import org.springframework.core.convert.converter.Converter;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.oauth2.jwt.Jwt;
    import org.springframework.stereotype.Component;
    
    import java.util.*;
    import java.util.stream.Collectors;
    
    
    @Component
    public class JwtGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
    
        @Override
        public Collection<GrantedAuthority> convert(Jwt jwt) {
            List<GrantedAuthority> roles = ((List<String>) jwt.getClaims().get("app_roles")).stream() 
                    .map(roleName -> "ROLE_" + roleName)
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
            List<GrantedAuthority> scopes = ((List<String>)jwt.getClaims().get("app_scopes")).stream()
                    .map(scopeName -> "SCOPE_" + scopeName)
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
            List<GrantedAuthority> authorities = new ArrayList<>(scopes);
            authorities.addAll(roles);
            return authorities;
        }
    }
    

    Then we just need to wire the JwtGrantedAuthoritiesConverter with our SpringSecurity configuration (FilterChain

    @Configuration
    @EnableMethodSecurity
    public class SecurityConfig {
    
        private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter;
    
        public SecurityConfig(JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter) {
            this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter;
        }
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(authorize -> authorize
                            .requestMatchers("swagger-ui/**", "/v3/api-docs/**").permitAll()
                            .requestMatchers("/test").access(AuthorizationManagers.allOf(
                                    AuthorityAuthorizationManager.hasAuthority("SCOPE_super"),
                                    AuthorityAuthorizationManager.hasAuthority("ROLE_adminit")
                            ))
                            .anyRequest().authenticated())
                    .oauth2ResourceServer(oauth2 -> oauth2
                            .jwt(jwt -> jwt.jwtAuthenticationConverter(customJwtAuthenticationConverter())));
    
            return http.build();
        }
    
        private JwtAuthenticationConverter customJwtAuthenticationConverter() {
            JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
            converter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
            return converter;
        }
    
    }
    
    

    Now we are able to manage the authorization ! The user's token should have SCOPE_super and ROLE_adminit to be able to access to the /test -secured- endpoint 👏

    Obviously we can also use @PreAuthorize at the method level.