1. Overview
In this tutorial, we’ll explore options for testing access control rules with mocked identities in a Spring application with OAuth2 security.
We’ll use MockMvc request post-processors, WebTestClient mutators, and test annotations, from both spring-security-test and spring-addons.
2. Why Using Spring-Addons?
In the field of OAuth2, spring-security-test only offers request post-processors and mutators that require the context of respectively a MockMvc or WebTestClient request. This can be just fine for @Controllers, but it is an issue to test method security (@PreAuthorize, @PostFilter, etc.) on a @Service or @Repository.
Using annotations like @WithJwt or @WithOidcLogin, we can mock the security context when unit testing any kind of @Component in both servlet and reactive applications. This is why we’ll use spring-addons-oauth2-test during some of our tests: it provides us with such annotations for most of Spring OAuth2 Authentication implementations.
3. What Will We Test?
The companion GitHub repository contains two resource servers sharing the following features:
- secured with a JWT decoder (rather than opaque token introspection)
- require ROLE_AUTHORIZED_PERSONNEL authority to access /secured-route and /secured-method
- return 401 if authentication is missing or invalid (expired, wrong issuer, etc.) and 403 if access is denied (missing roles)
- define access control using Java configuration (with requestMatcher and pathMatcher for servlet and reactive app, respectively) and method security
- use data from the Authentication in the security context to build response payloads
To illustrate the slight differences between servlet and reactive test APIs, one is a servlet (browse code), and the second is a reactive application (browse code).
In this article, we’ll focus on testing access control rules in unit and integration tests and assert that the HTTP status of the response matches the expectations according to mocked user identities, or that an exception is thrown when unit testing other @Component than @Controller, like @Service or @Repository secured with @PreAuthorize, @PostFilter and alike.
All tests pass without any authorization server, but we’ll need one to be up and running if we ever like to start the resource servers under test and query it with tools like Postman. A Docker Compose file for a local Keycloak instance is provided to get started quickly:
- admin console available from: http://localhost:8080/admin/master/console/#/baeldung
- admin account is admin / admin
- a baeldung realm is created already with a confidential client (baeldung_confidential / secret) an two users (authorized and forbidden, both with secret as secret)
4. Unit Testing With Mocked Authentications
By “unit test”, we mean a test of a single @Component in isolation of any other dependency (which we’ll mock). The tested @Component could be a @Controller in a @WebMvcTest or @WebFluxTest, or as any other secured @Service, @Repository, etc., in a plain JUnit test.
MockMvc and WebTestClient ignore the Authorization header, and there’s no need to provide a valid access token. Of course, we could instantiate or mock any authentication implementation and manually create a security context at the beginning of each test, but this is way too tedious. Instead, we’ll use spring-security-test MockMvc request post-processors, WebTestClient mutators, or spring-addons annotations to populate the test security context with a mocked Authentication instance of our choice.
We’ll use @WithMockUser just to see that it builds a UsernamePasswordAuthenticationToken instance which is frequently an issue as OAuth2 runtime configuration puts other types of Authentication in the security context:
- JwtAuthenticationToken for resource server with a JWT decoder
- BearerTokenAuthentication for resource server with access token introspection (opaqueToken)
- OAuth2AuthenticationToken for clients with oauth2Login
- Absolutely anything if we decide to return another Authentication instance than Spring default one in a custom authentication converter. So, technically, it’s possible for an OAuth2 authentication converter to return a UsernamePasswordAuthenticationToken instance and use @WithMockUser in tests, but it’s a pretty unnatural choice, and we won’t use that here.
4.1. Important Notes
MockMvc pre-processors and WebTestClient mutators don’t use the beans defined in the security configuration to build the test Authentication instances. As a consequence, defining OAuth2 claims with SecurityMockMvcRequestPostProcessors.jwt() or SecurityMockServerConfigurers.mockJwt() won’t have any impact on authentication name and authorities. We have to set the name and authorities by ourselves, using the dedicated methods.
As a contrast, the factories behind spring-addons annotations scan the test context for an authentication converter and use it if they find one. So, when using @WithJwt it’s important to expose any custom JwtAuthenticationConverter as a bean (instead of just inlining it as a lambda in security conf):
@Configuration
@EnableMethodSecurity
@EnableWebSecurity
static class SecurityConf {
@Bean
SecurityFilterChain filterChain(HttpSecurity http, Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) throws Exception {
http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(jwtResourceServer -> jwtResourceServer.jwtAuthenticationConverter(authenticationConverter)));
...
}
@Bean
JwtAuthenticationConverter authenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
final var authenticationConverter = new JwtAuthenticationConverter();
authenticationConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME);
authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return authenticationConverter;
}
}
Notably, the authentication converter is exposed as a @Bean which is explicitly injected in the security filter-chain. That way, the factory behind @WithJwt can use it to build the Authentication from the claims, the exact same way it would be at runtime with a real token.
Also note that in the advanced cases were the the authentication converter returns something else than a JwtAuthenticationToken (or BearerTokenAuthentication in the case of a resource server with token introspection), only the test annotation from Spring addons will build the expected type of Authentication.
4.2. Test Setup
For @Controller unit tests, we should decorate test classes with @WebMvcTest for servlet apps and @WebFluxTest for reactive ones.
Spring autowires MockMvc or WebTestClient for us, and as we’re writing controller unit tests, we’ll mock MessageService.
This is what an empty @Controller unit test would look like in a servlet application:
@WebMvcTest(controllers = GreetingController.class)
class GreetingControllerTest {
@MockBean
MessageService messageService;
@Autowired
MockMvc mockMvc;
//...
}
4.5. Unit Testing Controllers With Annotations from Spring-Addons
We can use test annotations in the exact same way in servlet and reactive apps.
All we need is to add a dependency on spring-addons-oauth2-test:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-oauth2-test</artifactId>
<version>7.6.12</version>
<scope>test</scope>
</dependency>
This library comes with quite a few annotations covering the following use-cases:
- @WithMockAuthentication is frequently enough when testing role based access-control: it is intended to take authorities as parameter, but also accepts a user name and implementation types to mock for Authentication and Principal.
- @WithJwt to use when testing a resource server with JWT decoder. It relies on an authentication factory which picks the Converter<Jwt, ? extends AbstractAthenticationToken> (or Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> in reactive applications) from the security configuration, and a JSON payload on the test classpath. This gives a complete hand on claims and provides with the same authentication instance as we would have at runtime for the same JWT payload.
- @WithOpaqueToken works the same as @WithJwt, but for resource servers with token introspection: it relies on a factory picking the OpaqueTokenAuthenticationConverter (or ReactiveOpaqueTokenAuthenticationConverter).
- @WithOAuth2Login and @WithOidcLogin will be our choice when what we want to test is an OAuth2 client with login
Before getting into the tests, we will define some JSON files as test resources. It’s intended to mock the JSON payload (or introspection response) of access tokens for representative users (personas or personae). We may copy the payload of real tokens using tools like https://jwt.io.
Ch4mpy will be our test user with the AUTHORIZED_PERONNEL role:
{
"iss": "https://localhost:8443/realms/master",
"sub": "281c4558-550c-413b-9972-2d2e5bde6b9b",
"iat": 1695992542,
"exp": 1695992642,
"preferred_username": "ch4mpy",
"realm_access": {
"roles": [
"admin",
"ROLE_AUTHORIZED_PERSONNEL"
]
},
"email": "[email protected]",
"scope": "openid email"
}
And we’ll define a second user without AUTHORIZED_PERONNEL role:
{
"iss": "https://localhost:8443/realms/master",
"sub": "2d2e5bde6b9b-550c-413b-9972-281c4558",
"iat": 1695992551,
"exp": 1695992651,
"preferred_username": "tonton-pirate",
"realm_access": {
"roles": [
"uncle",
"skipper"
]
},
"email": "[email protected]",
"scope": "openid email"
}
Now, we can remove identity mocking from the test body, decorating the test method with an annotation instead. For demonstration purpose, we’ll use both @WithMockAuthentication and @WithJwt, but one would be enough in actual tests. We’d probably choose the first when we need to define just authorities or name, and the second when we need to have a hand on many claims:
@Test
@WithAnonymousUser
void givenRequestIsAnonymous_whenGetSecuredMethod_thenUnauthorized() throws Exception {
api.perform(get("/secured-method"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockAuthentication({ "admin", "ROLE_AUTHORIZED_PERSONNEL" })
void givenUserIsGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenOk() throws Exception {
final var secret = "Secret!";
when(messageService.getSecret()).thenReturn(secret);
api.perform(get("/secured-method"))
.andExpect(status().isOk())
.andExpect(content().string(secret));
}
@Test
@WithMockAuthentication({ "admin" })
void givenUserIsNotGrantedWithRoleAuthorizedPersonnel_whenGetSecuredMethod_thenForbidden() throws Exception {
api.perform(get("/secured-method"))
.andExpect(status().isForbidden());
}
@Test
@WithJwt("ch4mpy.json")
void givenUserIsCh4mpy_whenGetSecuredMethod_thenOk() throws Exception {
final var secret = "Secret!";
when(messageService.getSecret()).thenReturn(secret);
api.perform(get("/secured-method"))
.andExpect(status().isOk())
.andExpect(content().string(secret));
}
@Test
@WithJwt("tonton-pirate.json")
void givenUserIsTontonPirate_whenGetSecuredMethod_thenForbidden() throws Exception {
api.perform(get("/secured-method"))
.andExpect(status().isForbidden());
}
Annotations definitely fit very well with the BDD paradigm:
- preconditions (Given) are in text context (annotation decorating the test)
- only tested code execution (When) and result assertions (Then) are in the test body
4.6. Unit Testing @Service or @Repository Secured Method
When testing @Controller, the choice between request MockMvc post-processors (or WebTestClient mutators) and annotations is mostly a matter of team preference, but to unit test MessageService::getSecret access control, spring-security-test is no longer an option, and we’ll need spring-addons annotations.
Here is the JUnit setup:
- activate Spring auto-wiring with @ExtendWith(SpringExtension.class)
- import and autowire the MessageService to get an instrumented instance
- if using @WithJwt, we need to import the configuration containing the JwtAuthenticationConverter as well as the AuthenticationFactoriesTestConf. Otherwise, decorating the test with @EnableMethodSecurity is enough.
We’ll assert that MessageService throws an exception each time the user is missing the ROLE_AUTHORIZED_PERSONNEL authority.
Here is a complete unit test of a @Service in a servlet application:
@ExtendWith(SpringExtension.class)
@TestInstance(Lifecycle.PER_CLASS)
@Import({ MessageService.class, SecurityConf.class })
@ImportAutoConfiguration(AuthenticationFactoriesTestConf.class)
class MessageServiceUnitTest {
@Autowired
MessageService messageService;
@MockBean
JwtDecoder jwtDecoder;
@Test
void givenSecurityContextIsNotSet_whenGreet_thenThrowsAuthenticationCredentialsNotFoundException() {
assertThrows(AuthenticationCredentialsNotFoundException.class, () -> messageService.getSecret());
}
@Test
@WithAnonymousUser
void givenUserIsAnonymous_whenGreet_thenThrowsAccessDeniedException() {
assertThrows(AccessDeniedException.class, () -> messageService.getSecret());
}
@Test
@WithJwt("ch4mpy.json")
void givenUserIsCh4mpy_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities() {
assertEquals("Hello ch4mpy! You are granted with [admin, ROLE_AUTHORIZED_PERSONNEL].",
messageService.greet());
}
@Test
@WithMockUser(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, username = "ch4mpy")
void givenSecurityContextIsPopulatedWithUsernamePasswordAuthenticationToken_whenGreet_thenThrowsClassCastException() {
assertThrows(ClassCastException.class, () -> messageService.greet());
}
}
A unit test of a @Service in a reactive application is not much different:
@ExtendWith(SpringExtension.class)
@TestInstance(Lifecycle.PER_CLASS)
@Import({ MessageService.class, SecurityConf.class })
@ImportAutoConfiguration(AuthenticationFactoriesTestConf.class)
class MessageServiceUnitTest {
@Autowired
MessageService messageService;
@MockBean
ReactiveJwtDecoder jwtDecoder;
@Test
void givenSecurityContextIsEmpty_whenGreet_thenThrowsAuthenticationCredentialsNotFoundException() {
assertThrows(AuthenticationCredentialsNotFoundException.class, () -> messageService.greet()
.block());
}
@Test
@WithAnonymousUser
void givenUserIsAnonymous_whenGreet_thenThrowsClassCastException() {
assertThrows(ClassCastException.class, () -> messageService.greet()
.block());
}
@Test
@WithJwt("ch4mpy.json")
void givenUserIsCh4mpy_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities() {
assertEquals("Hello ch4mpy! You are granted with [admin, ROLE_AUTHORIZED_PERSONNEL].",
messageService.greet().block());
}
@Test
@WithMockUser(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, username = "ch4mpy")
void givenSecurityContextIsPopulatedWithUsernamePasswordAuthenticationToken_whenGreet_thenThrowsClassCastException() {
assertThrows(ClassCastException.class, () -> messageService.greet().block());
}
}
4.7. JUnit 5 @ParametrizedTest
JUnit 5 allows to define tests that run several times with different values for a parameter. This parameter can be the mocked Authentication to put in the security context.
@WithMockAuthentication builds the authentication instance independently of Spring context, which makes it very easy to use in parametrized tests:
@ParameterizedTest
@AuthenticationSource({
@WithMockAuthentication(authorities = { "admin", "ROLE_AUTHORIZED_PERSONNEL" }, name = "ch4mpy"),
@WithMockAuthentication(authorities = { "uncle", "PIRATE" }, name = "tonton-pirate") })
void givenUserIsAuthenticated_whenGetGreet_thenOk(@ParameterizedAuthentication Authentication auth) throws Exception {
final var greeting = "Whatever the service returns";
when(messageService.greet()).thenReturn(greeting);
api.perform(get("/greet"))
.andExpect(status().isOk())
.andExpect(content().string(greeting));
verify(messageService, times(1)).greet();
}
The points to note in the code above are the following:
- use @ParameterizedTest instead of @Test
- decorate the test with @AuthenticationSource containing an array of all the @WithMockAuthentication to use
- add a @ParameterizedAuthentication parameter to the test method
Because @WithJwt uses a bean from the application context to build Authentication instances, we have a little more to do:
@TestInstance(Lifecycle.PER_CLASS)
class MessageServiceUnitTest {
@Autowired
WithJwt.AuthenticationFactory authFactory;
private Stream<AbstractAuthenticationToken> allIdentities() {
final var authentications = authFactory.authenticationsFrom("ch4mpy.json", "tonton-pirate.json").toList();
return authentications.stream();
}
@ParameterizedTest
@MethodSource("allIdentities")
void givenUserIsAuthenticated_whenGreet_thenReturnGreetingWithPreferredUsernameAndAuthorities(@ParameterizedAuthentication Authentication auth) {
final var jwt = (JwtAuthenticationToken) auth;
final var expected = "Hello %s! You are granted with %s.".formatted(jwt.getTokenAttributes().get(StandardClaimNames.PREFERRED_USERNAME), auth.getAuthorities());
assertEquals(expected, messageService.greet());
}
}
Our checklist when using @ParameterizedTest with @WithJwt should be the following:
- decorate the test class with @TestInstance(Lifecycle.PER_CLASS)
- autowire the WithJwt.AuthenticationFactory
- define a method returning a stream of the authentication to use, using the authentication factory for each
- use @ParameterizedTest instead of @Test
- decorate the test with @MethodSource referencing the method defined above
- add a @ParameterizedAuthentication parameter to the test method
5. Integration Testing With Mocked Authorizations
We’ll write Spring Boot integration tests with @SpringBootTest so that Spring wires actual components together. To keep using mocked identities, we’ll use it with MockMvc or WebTestClient. The tests themself and options to populate the test security context with mocked identities are the same as for unit tests. Only test setup changes:
- No more components mock nor argument matcher
- We’ll use @SpringBootTest(webEnvironment = WebEnvironment.MOCK) instead of @WebMvcTest or @WebFluxTest. The MOCK environment is the best match for mocked authorizations with MockMvc or WebTestClient
- decorate explicitly test class with @AutoConfigureMockMvc or @AutoConfigureWebTestClient for MockMvc or WebTestClient injection
Here is the skeleton for a Spring Boot servlet integration test:
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
class ServletResourceServerApplicationTests {
@Autowired
MockMvc api;
// Test structure and mocked identities options are the same as seen before in unit tests
}
And this is its equivalent in a reactive application:
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureWebTestClient
class ReactiveResourceServerApplicationTests {
@Autowired
WebTestClient api;
// Test structure and mocked identities options are the same as seen before in unit tests
}
Of course, this kind of integration test saves the configuration of mocks, argument captors, etc., but it is also slower and much more fragile than unit tests. We should use it with caution, maybe with lower coverage than @WebMvcTest or @WebFluxTest, just to assert that auto-wiring and inter-component communication work.
6. To Go Further
So far, we tested resource servers secured with a JWT decoder, which have JwtAuthenticationToken instances in the security context. We only ran automated tests with mocked HTTP requests without involving any authorization server in the process.
6.1. Testing With Any Type of OAuth2 Authentication
As seen earlier, Spring OAuth2 security context can hold other types of Authentication, in which case, we should use other annotations, request post-processors or mutators in tests:
- By default, resource servers with token introspection have BearerTokenAuthentication instances in their security context, and tests should use @WithOpaqueToken, opaqueToken(), or mockOpaqueToken()
- Clients with oauth2Login(), usually have an OAuth2AuthenticationToken in their security context and we’d use @WithOAuth2Login, @WithOidcLogin, oauth2Login(), oidcLogin(), mockOAuth2Login() or mockOidcLogin()
- Suppose we explicitly configure a custom Authentication type with http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(…) or whatever. In that case, we could have to provide our own unit test tooling, which is not that complicated when using spring-addons implementations as a sample. The same Github repo also contains samples with custom Authentication and dedicated test annotation
6.2. Running the Sample Applications
The sample projects contain properties for the master realm of a Keycloak instance running at https://localhost:8443. Using any other OIDC authorization server would require no more than adapting issuer-uri property and the authorities mapper in Java config: change realmRoles2AuthoritiesConverter bean to map authorities from the private claim(s) the new authorization server puts roles into.
For more details about Keycloak setup, refer to the official getting started guides. The one for standalone zip distribution might be the easiest to start with.
To set up a local Keycloak instance with TLS using a self-signed certificate, this GitHub repo could be super useful.
The authorization server should have a minimum of:
- Two declared users, one being granted the ROLE_AUTHORIZED_PERSONNEL and not the other
- A declared client with authorization code flow enabled for tools like Postman to get access tokens on behalf of those users
7. Conclusion
In this article, we explored two options for unit and integration testing Spring OAuth2 access control rules with mocked identities in both servlet and reactive applications:
- MockMvc request post-processors and WebTestClient mutators from spring-security-test
- OAuth2 test annotations from spring-addons-oauth2-test
We also saw that we could test @Controllers with MockMvc request post-processors, WebTestClient mutators, or annotations. However, only the latter enables us to set the security context when testing other types of components.
As always, we can find this source code over on GitHub.