Spring offers many methods for checking authorization. In this short blog post I will focus on
checking authorization at method-level and at the level of individual users.
First, we need to enable @PreAuthorize
and @PostAuthorize
annotations which are required for checking method-level security, by adding @EnableGlobalMethodSecurity(prePostEnabled = true)
to a @Configuration
bean that extends GlobalMethodSecurityConfiguration
[1].
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}
|
Now we can make use of Spring Security’s DSL to check whether the roles are fulfilled at individual methods:
@PreAuthorize("hasRole('DISPATCHER')")
@DeleteMapping("{id}")
public void deleteUser(@PathVariable String id) {
...
}
|
If we do not like having to type at each method hasRole('ROLENAME')
we could also create our own annotation like this:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('DISPATCHER')")
public @interface IsDispatcher {
}
|
With this version typos can only occur where we define the annotation. The only downside is that potentially a lot of annotation classes need to be created.
Checking multiple roles works very similar to checking a single role. Just use hasAnyRole
:
@PreAuthorize("hasAnyRole('DELIVERER, DISPATCHER')")
@GetMapping("{id}")
public Delivery findDeliveryById(@PathVariable String id) {
...
}
|
I would generally recommend avoiding this annotation altogether, as it only checks authorization after the execution of the method body.
In the previous blog post JWT authentication was shown without explaining the need for including a user ID in the JWT token. We will show these parts now again and explain how we can use them to check whether individual users are authorized to access certain endpoints.
This is an extract of the JWTUtils
that shows how we can put the user id into the token.
public String generateToken(AuthenticatedUser user) {
Map<String, Object> claims = new HashMap<>();
claims.put(ROLES, user.getAuthorities());
claims.put(USER_ID, user.getUserId());
return createToken(claims, user.getUsername());
}
|
Of course, we also need to extract it then again in the filter:
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
...
String userName = jwtUtils.extractUsername(token);
String userId = jwtUtils.extractUserId(token);
if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
var authorities = jwtUtils.extractRole(token);
AuthenticatedUser authenticatedUser = new AuthenticatedUser(userId, userName, "", authorities);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(authenticatedUser, null, authorities);
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
...
}
|
The result is stored in a AuthenticatedUser
object that is a simple subclass of Spring’s UserDetails
class.
The AuthenticatedUser
object is then stored in the UsernamePasswordAuthenticationToken
which in turn is used to
set the authentication with the SecurityContextHolder
.
@Getter
public class AuthenticatedUser extends org.springframework.security.core.userdetails.User {
private final String userId;
public AuthenticatedUser(String userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.userId = userId;
}
}
|
Now we can check the authorization at user level as follows. First, we create some static helper methods:
public class AuthUtils {
public static AuthenticatedUser getAuthenticatedUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof AuthenticatedUser) {
return (AuthenticatedUser) auth.getPrincipal();
}
// auth must be non-null and the authenticated user must be set due to the filter
throw new InternalServerError("Is the JwtFilter enabled?");
}
public static boolean isAuthenticatedAs(UserRole role) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
return auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_" + role.name()));
}
// auth must be non-null due to the filter
throw new InternalServerError("Is the JwtFilter enabled?");
}
}
|
In these methods we can safely assume that the Authentication
object retrieved from the security context must be non-null as it is set in our filter. If it is not set, we throw an InternalServerError
as this indicates that we forgot to enable the JwtFilter
.
Also, due to the same assumption, we can cast to AuthenticatedUser
.
Finally, we can perform the actual authorization check as follows:
@GetMapping("{id}")
public Delivery findDeliveryById(@PathVariable String id) {
var delivery = deliveryService.findDeliveryByIdOrElseThrow(id);
var authenticatedUser = getAuthenticatedUser();
if (isAuthenticatedAs(UserRole.USER) && authenticatedUser.getUserId().equals(delivery.getTargetCustomer())) {
return delivery;
} else if (isAuthenticatedAs(UserRole.DELIVERER) && authenticatedUser.getUserId().equals(delivery.getDeliverer())) {
return delivery;
} else if (isAuthenticatedAs(UserRole.DISPATCHER)) {
return delivery;
}
throw new AccessDeniedException("User is not allowed to view this delivery");
}
|