I am writing a unit test for spring security and JWT validation. There are 3 basic cases I want to start the test with:
I tested my code using postman and they return expected responses. In my unit test I have this:
@SpringBootTest
@ContextConfiguration(classes = SecurityConfig.class)
@AutoConfigureMockMvc
@EnableAutoConfiguration
@Import(DataImporterControllerConfig.class)
public class SecurityConfigTest {
@Autowired
private MockMvc mockMvc;
@Test
void doImportExpect200() throws Exception {
mockMvc.perform(put(URI).with(jwt().authorities(new SimpleGrantedAuthority(
"SCOPE_data:write"))).contentType(APPLICATION_JSON_VALUE).accept(APPLICATION_JSON)
.content(BODY)).andExpect(status().isOk());
}
It does pass validation part and try to return some value from the controller:
@RestController
@RequestMapping(value = "${data.uriPrefix}")
@Loggable(value = INFO, trim = false)
public class DataImporterController {
private final DataImporterService dataImporterService;
@PutMapping(path = "/someurl", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE)
public dataImportResponse doImport(@RequestHeader(name = ACCEPT, required = true) final String acceptHeader,
final @RequestBody @NotBlank String body) {
return new DataImportResponse(dataImporterService.doImport(body));
}
The logic inside dataImporterService.doImport(body)
require some db operation, so ideally, I want to mock it and make it return some value (something like when(dataImporterServiceMock.doImport(body)).thenReturn(something)
.
However, when I try it, it doesn't work. I think it is because I am not creating a controller with mocked service. I tried to create one, but due to configuration for SecurityConfig
class, it is not that easy. Here is SecurityConfig class:
@EnableWebSecurity
public class SecurityConfig {
@Value("${auth0.audience}")
private String audience;
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
/*
This is where we configure the security required for our endpoints and setup our app to serve as
an OAuth2 Resource Server, using JWT validation.
*/
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/actuator/**").permitAll()
.antMatchers(HttpMethod.PUT, "/dataimporter/**").hasAuthority("SCOPE_data:write")
.anyRequest().authenticated()
.and().cors()
.and().oauth2ResourceServer().jwt();
return http.build();
}
@Bean
JwtDecoder jwtDecoder() {
/*
By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
indeed intended for our app. Adding our own validator is easy to do:
*/
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromOidcIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
}
I got this code from Auth0 and just modified antMatcher
portion. How can I test it so it returns 200 (mock service or something)?
As already stated in the answer to your other question "How to write unit test for SecurityConfig for spring security", for unit-testing a @Controller
, use @WebMvcTest
(with mocked dependencies), not @SpringBootTest
which is intended for integration testing, loads much more config, and instantiate & autowire actual components (and, as a consequence, is slower and less focused)
@WebMvcTest(controllers = DataImporterController.class)
public class DataImporterControllerUnitTest {
@MockBean
DataImporterService dataImporterService;
@Autowired
private MockMvc mockMvc;
@BeforeEach
public void setUp() {
when(dataImporterService.doImport(body)).thenReturn(something);
}
@Test
void doImportExpect200() throws Exception {
mockMvc.perform(put(URI).with(jwt().authorities(new SimpleGrantedAuthority(
"SCOPE_data:write"))).contentType(APPLICATION_JSON_VALUE).accept(APPLICATION_JSON)
.content(BODY)).andExpect(status().isOk());
}
}
If you had followed the link I already provided, you'd have find that in the main README, in the many samples, and in the tutorials
I am writing a unit test for spring security and JWT validation.
This are completely separate concerns and is to be done separately:
@Controller
of course, but also @Service
, @Repository
, etc. where method-security is used (refer to your previous question for instructions and to my repo for samples)@WebMvcTest
or @WebfluxTest
security-context. Authentication instances are built based on MockMvc request post-processors, WebTestClient mutators or annotations and this does not involve any token decoding or validation. JWT validation has to be tested in a JwtDecoder
unit-test (only if you keep your own, which you shouldn't, see below)Spring Security does not validate the "aud" claim of the token
This is wrong with spring-boot since 2.7: spring.security.oauth2.resourceserver.jwt.audiences
property does just that. There is no need for you to override the JwtDecoder
provided by spring-boot (actually, you shouldn't), and, as a consequence to unit-test your own.
- When no token -> expect 401
- When token but wrong scope -> expect 403
- When token and scope -> expect 200
To be exact, the phrasing would better be:
The reason for that are:
Authentication
instance: by default AnonymousAuthenticationToken
if token is missing or validation fails, and whatever the configured authentication-converter returns if validation is successfulGrantedAuthority
, not scope
. This common confusion is due to the fact that there is nothing related to RBAC in OAuth2 nor OpenID standards and the default authentication-converter had to choose a claim that is always there to map authorities from. scope
claim was picked as default, adding the SCOPE_
prefix. You should refer to how RBAC is implemented by your own authorization server and provide an authentication-converter bean to map authorities from the right claim(s).Auth0 uses roles
and permissions
claims when RBAC is enabled, Keycloak uses realm_access.roles
and resource_access.{client-id}.roles
, etc., reason for me implementing a configurable authorities-converter, which picks the claims that should be used as authorities source from application properties (and how to map it: prefix and case transformation).
Last, your configuration is still risky (enabled sessions with disabled CSRF protection and poor CORS config). You should really consider using "my" starters, or follow the tutorial I wrote for configuring a resource-server with just spring-boot-oauth2-resource-server
.