You can test actuator endpoints access-control in integration tests only (@SpringBootTest
). For your own secured @Components
, you can do it also in unit-tests (many samples in this repo):
@Controller
with @WebMvcTest
(@WebfluxTest
if you were in a reactive app)
- plain JUnit with
@ExtendWith(SpringExtension.class)
, @EnableMethodSecurity
and @Import
of the tested component (@Service
or @Repository
with method security like @PreAuthorize
expressions) to get an autowired instance instrumented with security
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 in an integration test (@SpringBootTest
) for actuator to be up (but you get the idea for unit-tests):
<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;
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
class ApplicationIntegrationTest {
@Autowired
MockMvc api;
@Test
void givenUserIsAnonymous_whenGetLiveness_thenOk() throws Exception {
api.perform(get("/data/actuator/health/liveness"))
.andExpect(status().isOk());
}
@Test
void givenUserIsAnonymous_whenGetMachin_thenUnauthorized() throws Exception {
api.perform(get("/data/machin"))
.andExpect(status().isUnauthorized());
}
@Test
void givenUserIsGrantedWithDataWrite_whenGetMachin_thenOk() throws Exception {
api.perform(get("/data/machin")
.with(jwt().jwt(jwt -> jwt.authorities(List.of(new SimpleGrantedAuthority("SCOPE_data:write"))))))
.andExpect(status().isOk());
}
@Test
void givenUserIsAuthenticatedButNotGrantedWithDataWrite_whenGetMachin_thenForbidden() throws Exception {
api.perform(get("/data/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>
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
class ApplicationIntegrationTest {
@Autowired
MockMvc api;
@Test
void givenUserIsAnonymous_whenGetLiveness_thenOk() throws Exception {
api.perform(get("/data/actuator/health/liveness"))
.andExpect(status().isOk());
}
@Test
void givenUserIsAnonymous_whenGetMachin_thenUnauthorized() throws Exception {
api.perform(get("/data/machin"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockJwtAuth("SCOPE_data:write")
void givenUserIsGrantedWithDataWrite_whenGetMachin_thenOk() throws Exception {
api.perform(get("/data/machin"))
.andExpect(status().isOk());
}
@Test
@WithMockJwtAuth("SCOPE_openid")
void givenUserIsAuthenticatedButNotGrantedWithDataWrite_whenGetMachin_thenForbidden() throws Exception {
api.perform(get("/data/machin"))
.andExpect(status().isForbidden());
}
}
Spring-addons starter
In the same repo as test annotations, you'll find starters to simplify your resource server security config (and also improve your CORS config and 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 to switch to another OIDC authorization-server would be properties. This could happen for instance because you are forced to by the busyness (if, they decide that Auth0 is too expensive or cannot be trusted anymore) or maybe because you find it is more convenient to use a standalone Keycloak on your dev machine (it is available offline, which I frequently am).
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>
By default, users must be authenticated to access any route but those listed in com.c4-soft.springaddons.security.permit-all
property (see below). Replace all your Java conf with:
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
// If not using method-security or to configure actuator RBAC
// You might define a bean of type ExpressionInterceptUrlRegistryPostProcessor
// and Fine tune AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry
}
You can remove all spring.security.oauth2.resourceserver
properties, it is ignored (with an exception of spring.security.oauth2.resourceserver.jwt.audiences
if you validate audience). Properties to use instead are:
# Define this instead of auth0.audience
spring.security.oauth2.resourceserver.jwt.audiences=http://localhost:8080,https://localhost:8080
# Single OIDC JWT issuer but you can add as many as you like
com.c4-soft.springaddons.security.issuers[0].location=https://dev-ch4mpy.eu.auth0.com/
# Mimic spring-security default converter: map authorities with "SCOPE_" prefix
# Difference with your current conf is authorities source is not only "scope" claim but also "roles" and "permissions" ones
# I would consider map authorities without "SCOPE_" prefix (the default behaviour of my starters) and update access control expressions accordingly
com.c4-soft.springaddons.security.issuers[0].authorities.claims=scope,roles,permissions
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=/data/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=/data/actuator/**
Bootyfool, isn't it?