简体   繁体   中英

Spring Boot - How to unit test @Service class methods that use @PreAuthorize when using JWT with custom claims

We have a Spring Boot backend application that uses JWT authentication implemented using Spring Security with OAuth2 Resource Server. We have secured our web layer by limiting access to URL patterns based on user roles using antMatchers() and service layer methods using global method security.

Our JWT tokens are provided by Auth0, where we have configured to include additional user metadata as custom claims. These claims values are verified in @PreAuthorize annotations as follows for example:

    @PreAuthorize("authentication.principal.claims[@environment.getProperty('jwt.organizationIdClaim')] == @environment.getProperty('current.organizationId')")
    public void deleteUser(String userId) {

Here the properties jwt.organizationIdClaim and current.organizationId are custom properties obtained from the application.properties file.

When I attempt to unit test the above method, I get an exception as follows:

org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext

Unit test code:

    @Test
    void testDoNotAllowDeletingUsersWithRootRole() {
        auth0ManagementAPIService.deleteUser("auth0|REDACTED");
        assertTrue(true);
    }

I tried to run this test using @WithMockUser , but this results in the @PreAuthorize expression unable to be evaluated:

java.lang.IllegalArgumentException: Failed to evaluate expression 'authentication.principal.claims[@environment.getProperty('jwt.organizationIdClaim')] == @environment.getProperty('current.organizationId')'

Is there any way to add these custom claim data to the SecurityContext for these unit tests so that the @PreAuthorize expression evaluates successfully?

We bypassed the JWT requirement in the web layer by using SecurityMockMvcRequestPostProcessors.jwt() , perhaps there is something similar we could use for @Service class methods?

A sample payload of the JWT that we use appears as follows:

{
  "https://dev.api.REDACTED.com/roles": [
    "RESEARCHER",
    "ADMIN",
    "ROOT"
  ],
  "https://dev.api.REDACTED.com/organizationId": "REDACTED",
  "https://dev.api.REDACTED.com/userId": "REDACTED",
  "iss": "https://dev.login.REDACTED.com/",
  "sub": "auth0|REDACTED",
  "aud": [
    "https://dev.api.REDACTED.com",
    "https://REDACTED.us.auth0.com/userinfo"
  ],
  "iat": 1660626647,
  "exp": 1660713047,
  "azp": "REDACTED",
  "scope": "openid profile email offline_access",
  "permissions": [
    ...
  ]
}

Here https://dev.api.REDACTED.com/roles , https://dev.api.REDACTED.com/organizationId and https://dev.api.REDACTED.com/userId are custom claims whose values we need to verify in the @PreAuthorize conditions.

The principal object is of type Jwt and is generated as follows using a custom converter:

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

import java.util.ArrayList;
import java.util.List;

public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

    private final String rolesClaimName;

    public CustomJwtAuthenticationConverter(String rolesClaimName) {
        this.rolesClaimName = rolesClaimName;
    }

    @Override
    public AbstractAuthenticationToken convert(Jwt jwt) {
        // Get roles claim from the JWT token and add it to a granted authorities list.
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        List<String> rolesList = jwt.getClaimAsStringList(rolesClaimName);
        rolesList.forEach(r -> grantedAuthorities.add(new SimpleGrantedAuthority(r)));
        return new JwtAuthenticationToken(jwt, grantedAuthorities, jwt.getSubject());
    }
}

Which is then injected into the security filter chain in the Security config:

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(customJwtGrantedAuthoritiesConverter());
        http
                .cors().and()
                .authorizeRequests(
                        authorize -> authorize
                                .antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                                .antMatchers("/actuator", "/actuator/health").permitAll()
                                .antMatchers(HttpMethod.DELETE, "/users/**").hasAuthority(ROOT_ROLE)
                                .antMatchers("/users/**").hasAnyAuthority(ADMIN_ROLE, ROOT_ROLE)
                                .anyRequest().authenticated());
        return http.build();
    }

    @Bean
    public Converter<Jwt, AbstractAuthenticationToken> customJwtGrantedAuthoritiesConverter() {
        return new CustomJwtAuthenticationConverter(rolesClaimName);
    }

I have exactly what you need there: https://github.com/ch4mpy/spring-addons

Sample here :

    @Test
    @WithMockJwtAuth(authorities = { "NICE", "AUTHOR" }, claims = @OpenIdClaims(preferredUsername = "Tonton Pirate"))
    void whenGrantedWithNiceRoleThenCanGreet() throws Exception {
        final var actual = mySecuredService.returnSomething();
        //test return falue
    }

@OpenIdClaims allows you to configure standard OpenID claims (but none is mandatory) and any private claim you like.

The advantage of test annotations over request post-processors is it works without MockMvc (which happens when you want to unit-test secured @Component which are not @Controller , like @Service or @Repository ). Unfortunately, spring-security team was not interested in it at time I contributed OAuth2 MockMvc post-processors (and WebTestClient mutators). Reason for me starting the lib linked above.

PS

You might find useful ideas in other tutorials . resource-server_with_oauthentication could save you quite some configuration code and resource-server_with_specialized_oauthentication could help you improve security expressions readability.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM