Test JwtDecoder in @WebMvcTest with Spring Security

Wim Deblauwe :

I am using Spring Boot 2.2.1 with spring-security-oauth2-resource-server:5.2.0.RELEASE. I want to write an integration test to test the security is ok.

I have this WebSecurityConfigurerAdapter defined in my application:

import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final OAuth2ResourceServerProperties properties;
    private final SecuritySettings securitySettings;

    public WebSecurityConfiguration(OAuth2ResourceServerProperties properties, SecuritySettings securitySettings) {
        this.properties = properties;
        this.securitySettings = securitySettings;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/api/**")
            .authenticated()
            .and()
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        NimbusJwtDecoder result = NimbusJwtDecoder.withJwkSetUri(properties.getJwt().getJwkSetUri())
                                                  .build();

        OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(
                JwtValidators.createDefault(),
                new AudienceValidator(securitySettings.getApplicationId()));

        result.setJwtValidator(validator);
        return result;
    }

    private static class AudienceValidator implements OAuth2TokenValidator<Jwt> {

        private final String applicationId;

        public AudienceValidator(String applicationId) {
            this.applicationId = applicationId;
        }

        @Override
        public OAuth2TokenValidatorResult validate(Jwt token) {
            if (token.getAudience().contains(applicationId)) {
                return OAuth2TokenValidatorResult.success();
            } else {
                return OAuth2TokenValidatorResult.failure(
                        new OAuth2Error("invalid_token", "The audience is not as expected, got " + token.getAudience(),
                                        null));
            }
        }
    }
}

It has a custom validator to check the audience (aud) claim in the token.

I currently have this test, which works, but it does not check the audience claim at all:

@WebMvcTest(UserController.class)
@EnableConfigurationProperties({SecuritySettings.class, OAuth2ResourceServerProperties.class})
@ActiveProfiles("controller-test")
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void testOwnUserDetails() throws Exception {
        mockMvc.perform(get("/api/users/me")
                                .with(jwt(createJwtToken())))
               .andExpect(status().isOk())
               .andExpect(jsonPath("userId").value("AZURE-ID-OF-USER"))
               .andExpect(jsonPath("name").value("John Doe"));
    }

    @Test
    void testOwnUserDetailsWhenNotLoggedOn() throws Exception {
        mockMvc.perform(get("/api/users/me"))
               .andExpect(status().isUnauthorized());
    }

    @NotNull
    private Jwt createJwtToken() {
        String userId = "AZURE-ID-OF-USER";
        String userName = "John Doe";
        String applicationId = "AZURE-APP-ID";

        return Jwt.withTokenValue("fake-token")
                  .header("typ", "JWT")
                  .header("alg", "none")
                  .claim("iss",
                         "https://b2ctestorg.b2clogin.com/80880907-bc3a-469a-82d1-b88ffad655df/v2.0/")
                  .claim("idp", "LocalAccount")
                  .claim("oid", userId)
                  .claim("scope", "user_impersonation")
                  .claim("name", userName)
                  .claim("azp", applicationId)
                  .claim("ver", "1.0")
                  .subject(userId)
                  .audience(Set.of(applicationId))
                  .build();
    }
}

I also have a properties file for the controller-test profile that contains the application id and the jwt-set-uri:

security-settings.application-id=FAKE_ID
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://b2ctestorg.b2clogin.com/b2ctestorg.onmicrosoft.com/discovery/v2.0/keys?p=b2c_1_ropc_flow

Maybe the JwtDecoder is not used because the Jwt is created manually? How could I make sure the JwtDecoder is called in the test?

Eleftheria Stein-Kousathana :

By using the JWT post processor .with(jwt(createJwtToken()))) you are able to bypass the JwtDecoder.

Consider what would happen if the JwtDecoder was not bypassed.
In the filter chain, your request would reach a point where the JwtDecoder parses the JWT value.
In this case the value is "fake-token", which will result in an exception because it is not a valid JWT.
This means the code will not even reach the point where AudienceValidator is called.

You can think of the value passed into SecurityMockMvcRequestPostProcessors.jwt(Jwt jwt) as the response that would be returned from JwtDecoder.decode(String token).
Then, the tests using SecurityMockMvcRequestPostProcessors.jwt(Jwt jwt) will test the behaviour when a valid JWT token is provided.
You can add additional tests for the AudienceValidator to ensure that it is functioning correctly.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=322699&siteId=1