Issue
We have a Spring Boot backend application that uses JWT authentication implemented using Spring Security with OAuth2 Resource Server. We have secured our web layer by limiting access to URL patterns based on user roles using antMatchers()
and service layer methods using global method security.
Our JWT tokens are provided by Auth0, where we have configured to include additional user metadata as custom claims. These claims values are verified in @PreAuthorize
annotations as follows for example:
@PreAuthorize("authentication.principal.claims[@environment.getProperty('jwt.organizationIdClaim')] == @environment.getProperty('current.organizationId')")
public void deleteUser(String userId) {
Here the properties jwt.organizationIdClaim
and current.organizationId
are custom properties obtained from the application.properties file.
When I attempt to unit test the above method, I get an exception as follows:
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
Unit test code:
@Test
void testDoNotAllowDeletingUsersWithRootRole() {
auth0ManagementAPIService.deleteUser("auth0|REDACTED");
assertTrue(true);
}
I tried to run this test using @WithMockUser
, but this results in the @PreAuthorize expression unable to be evaluated:
java.lang.IllegalArgumentException: Failed to evaluate expression 'authentication.principal.claims[@environment.getProperty('jwt.organizationIdClaim')] == @environment.getProperty('current.organizationId')'
Is there any way to add these custom claim data to the SecurityContext
for these unit tests so that the @PreAuthorize expression evaluates successfully?
We bypassed the JWT requirement in the web layer by using SecurityMockMvcRequestPostProcessors.jwt()
, perhaps there is something similar we could use for @Service class methods?
A sample payload of the JWT that we use appears as follows:
{
"https://dev.api.REDACTED.com/roles": [
"RESEARCHER",
"ADMIN",
"ROOT"
],
"https://dev.api.REDACTED.com/organizationId": "REDACTED",
"https://dev.api.REDACTED.com/userId": "REDACTED",
"iss": "https://dev.login.REDACTED.com/",
"sub": "auth0|REDACTED",
"aud": [
"https://dev.api.REDACTED.com",
"https://REDACTED.us.auth0.com/userinfo"
],
"iat": 1660626647,
"exp": 1660713047,
"azp": "REDACTED",
"scope": "openid profile email offline_access",
"permissions": [
...
]
}
Here https://dev.api.REDACTED.com/roles
, https://dev.api.REDACTED.com/organizationId
and https://dev.api.REDACTED.com/userId
are custom claims whose values we need to verify in the @PreAuthorize conditions.
The principal object is of type Jwt and is generated as follows using a custom converter:
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import java.util.ArrayList;
import java.util.List;
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final String rolesClaimName;
public CustomJwtAuthenticationConverter(String rolesClaimName) {
this.rolesClaimName = rolesClaimName;
}
@Override
public AbstractAuthenticationToken convert(Jwt jwt) {
// Get roles claim from the JWT token and add it to a granted authorities list.
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
List<String> rolesList = jwt.getClaimAsStringList(rolesClaimName);
rolesList.forEach(r -> grantedAuthorities.add(new SimpleGrantedAuthority(r)));
return new JwtAuthenticationToken(jwt, grantedAuthorities, jwt.getSubject());
}
}
Which is then injected into the security filter chain in the Security config:
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(customJwtGrantedAuthoritiesConverter());
http
.cors().and()
.authorizeRequests(
authorize -> authorize
.antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.antMatchers("/actuator", "/actuator/health").permitAll()
.antMatchers(HttpMethod.DELETE, "/users/**").hasAuthority(ROOT_ROLE)
.antMatchers("/users/**").hasAnyAuthority(ADMIN_ROLE, ROOT_ROLE)
.anyRequest().authenticated());
return http.build();
}
@Bean
public Converter<Jwt, AbstractAuthenticationToken> customJwtGrantedAuthoritiesConverter() {
return new CustomJwtAuthenticationConverter(rolesClaimName);
}
Solution
I have exactly what you need there: https://github.com/ch4mpy/spring-addons
Sample here:
@Test
@WithMockJwtAuth(authorities = { "NICE", "AUTHOR" }, claims = @OpenIdClaims(preferredUsername = "Tonton Pirate"))
void whenGrantedWithNiceRoleThenCanGreet() throws Exception {
final var actual = mySecuredService.returnSomething();
//test return falue
}
@OpenIdClaims
allows you to configure standard OpenID claims (but none is mandatory) and any private claim you like.
The advantage of test annotations over request post-processors is it works without MockMvc (which happens when you want to unit-test secured @Component
which are not @Controller
, like @Service
or @Repository
). Unfortunately, spring-security team was not interested in it at time I contributed OAuth2 MockMvc post-processors (and WebTestClient mutators). Reason for me starting the lib linked above.
PS
You might find useful ideas in other tutorials. resource-server_with_oauthentication
could save you quite some configuration code and resource-server_with_specialized_oauthentication
could help you improve security expressions readability.
Answered By - ch4mp
Answer Checked By - Mildred Charles (JavaFixing Admin)