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?
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.