For end-to-end or "smoke" tests (test which include more than just your resource-server: authorization-server, REST client, etc.), their are better suited tools than Java tests, and yes, this is very likely to require you to lift containers and program robots to manipulate UIs (perform user login, fill forms, etc.).
As stated by @Thorne in his answer, testing JWT decoding and validation should not be in the scope of your Java tests (unless you write your own decoder). Also exchange with an authorization-server to issue valid access-tokens would be way to slow and fragile for a decent coverage in unit-tests (JUnit with @ExtendWith(SpringExtension.class), @WebMvcTest
or @WebfluxTest
) or spring-boot integration-tests (@SpringBootTest
).
What I unit and integration test regarding security conf is access-control, using mocked OAuth2 identities.
spring-security-test
comes with some MockMvc request post-processors (see org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
in your case) as well as WebTestClient mutators (see org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt
) to configure Authentication of the right type (JwtAuthenticationToken
in your case) and set it in test security context, but this is limited to MockMvc and WebTestClient and as so to @Controller
tests.
Sample usage with your security conf and a @GetMapping("/api/v1/private/machin") @PreAuthorize("hasAuthority('SCOPE_api:read')")
:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
@WebMvcTest(controllers = MachinController.class, properties = { "server.ssl.enabled=false" })
class MachinControllerTest {
@Autowired
MockMvc api;
@Test
void givenUserIsAnonymous_whenGetMachin_thenUnauthorized() throws Exception {
api.perform(get("/api/v1/private/machin"))
.andExpect(status().isUnauthorized());
}
@Test
void givenUserIsGrantedWithApiRead_whenGetMachin_thenOk() throws Exception {
api.perform(get("/api/v1/private/machin")
.with(jwt().jwt(jwt -> jwt.authorities(List.of(new SimpleGrantedAuthority("SCOPE_api:read"))))))
.andExpect(status().isOk());
}
@Test
void givenUserIsAuthenticatedButNotGrantedWithApiRead_whenGetMachin_thenForbidden() throws Exception {
api.perform(get("/api/v1/private/machin")
.with(jwt().jwt(jwt -> jwt.authorities(List.of(new SimpleGrantedAuthority("SCOPE_openid"))))))
.andExpect(status().isForbidden());
}
}
You might also use @WithMockJwtAuth
from this libs I maintain. This repo contains quite a few samples for unit and integration testing of any kind of @Component
(@Controllers
of course but also @Services
or @Repositories
decorated with method-security).
Above Sample becomes:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-oauth2-test</artifactId>
<version>6.0.12</version>
<scope>test</scope>
</dependency>
@WebMvcTest(controllers = MachinController.class, properties = { "server.ssl.enabled=false" })
class MachinControllerTest {
@Autowired
MockMvc api;
@Test
void givenUserIsAnonymous_whenGetMachin_thenUnauthorized() throws Exception {
api.perform(get("/api/v1/private/machin"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockJwtAuth("SCOPE_api:read")
void givenUserIsGrantedWithApiRead_whenGetMachin_thenOk() throws Exception {
api.perform(get("/api/v1/private/machin"))
.andExpect(status().isOk());
}
@Test
@WithMockJwtAuth("SCOPE_openid")
void givenUserIsAuthenticatedButNotGrantedWithApiRead_whenGetMachin_thenForbidden() throws Exception {
api.perform(get("/api/v1/private/machin"))
.andExpect(status().isForbidden());
}
}
Spring-addons starter
In the same repo, you'll find starters to simplify your resource server security config (and also synchronize sessions and CSRF protection disabling as the second should not be disabled with active sessions...).
Usage is super simple and all you'd have to change between environments would be properties, even if using different OIDC authorization-servers, for instance a standalone Keycloak on your dev machine and a cloud provider like Cognito, Auth0, etc. in production.
Instead of directly importing spring-boot-starter-oauth2-resource-server
, import a thin wrapper around it (composed of 3 files only):
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.12</version>
</dependency>
As you already apply fine grained method-security to protected routes, you could probably empty this Java conf (keep default expressionInterceptUrlRegistryPostProcessor
bean which requires users to be authenticated to access any route but those listed in com.c4-soft.springaddons.security.permit-all
property):
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
ExpressionInterceptUrlRegistryPostProcessor expressionInterceptUrlRegistryPostProcessor() {
return (AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) -> registry
.requestMatchers("/api/v1/private/**").hasAnyAuthority("SCOPE_api:read", "SCOPE_api:write")
.anyRequest().authenticated();
}
}
Replace spring.security.oauth2.resourceserver
properties with:
# Single OIDC JWT issuer but you can add as many as you like
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
# Mimic spring-security default authorities converter: map from "scope" claim (accepts a comma separated list claims), with "SCOPE_" prefix
com.c4-soft.springaddons.security.issuers[0].authorities.claims=scope
com.c4-soft.springaddons.security.issuers[0].authorities.prefix=SCOPE_
# Fine-grained CORS configuration can be set per path as follow:
com.c4-soft.springaddons.security.cors[0].path=/api/**
com.c4-soft.springaddons.security.cors[0].allowed-origins=https://localhost,https://localhost:8100,https://localhost:4200
com.c4-soft.springaddons.security.cors[0].allowedOrigins=*
com.c4-soft.springaddons.security.cors[0].allowedMethods=*
com.c4-soft.springaddons.security.cors[0].allowedHeaders=*
com.c4-soft.springaddons.security.cors[0].exposedHeaders=*
# Comma separated list of ant path matchers for resources accessible to anonymous
com.c4-soft.springaddons.security.permit-all=/api/v1/public/**