Have you ever used Spring Security @PreAuthorize
annotation? It is quite common and widely used to define the required permissions/authorities for a method invocation.
It takes a string literal that usually looks like this @PreAuthorize("hasAuthority('read')"
). While this is convenient, there is something about this declaration that just does not feel right to me – a Java developer who likes compile time validation and type-safe solutions. This string literal feels like a method invocation – hasAuthority() – but it is not. It is some sort of SpEL expression telling Spring which method to invoke and pass it an argument ‘read’.
Thanks, but ‘No’. This ‘stringly-typed’ programming is not Javaish enough to me. And what if some similar errors sneak through the code reviews:
@PreAuthorize("hasAuthority('raed')")
@PreAuthorize("hasAutority('read')")
@PreAuthorize("hasAuthorities('read')")
Boom – now you have some authorization issues, and the smart IDE (Integrated Development Environment) will not tell you if there is a typo in the authority’s name or that such a method does not exist.
First of all, before going into the type-safe solution, let us have a look at some Spring Security internals.
How does @PreAuthorize
work?
This section is for the curious ones. If you are not interested in the details of how @PreAuthorize
works, you can skip straight to the solution section – How to implement type-save authorities?
TLDR, there is a before-method invocation logic that:
- Parses the ‘hasAuthorities()’ method name and the provided arguments/authorities.
- Invokes the method by passing the parsed authorities as arguments.
- Extracts the Authentication object.
- Compares the Authentication object’s GrantedAuthorities to the parsed ones.
- Throws AccessDeniedException if there is not a match.
1. Where is the @PreAuthorize
value extracted?
Basically, this expression registry:
- Looks for the
@PreAuthorize
annotation. - Fetches its value().
- Parses the value and constructs an Expression out of it.
2. How is this Expression used?
In PreAuthorizeAuthorizationManager.check(Supplier<Authentication> authentication, MethodInvocation mi):
- An
EvaluationContext
is constructed based on the incoming Authentication object.
- The Expression from the previous step is provided to the
ExpressionUtils.evaluateAsBoolean
method. - An
AuthorizationDecision
is constructed based on the boolean result from the expression evaluation.
What happens in the ExpressionUtils.evaluateAsBoolean is part of the SpringExpressionLanguage, which is not in the scope of this post. But the important part is that Spring Security specifics are abstracted out of the actual SpEL evaluation by using:
3. How is this EvaluationContext constructed?
SecurityExpressionOperations root = createSecurityExpressionRoot(authentication, invocation); StandardEvaluationContext ctx = createEvaluationContextInternal(authentication, invocation);
And this delegates to DefaultMethodSecurityExpressionHandler which performs
MethodSecurityExpressionRoot root = new MethodSecurityExpressionRoot(authentication);
4. What is MethodSecurityExpressionRoot?
First of all, its definition:
class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations
It inherits the declaration of boolean hasAuthority(String authority) from MethodSecurityExpressionOperations, which extends SecurityExpressionOperations
But the actual implementation comes from SecurityExpressionRoot.
This implementation delegates to a private method, which basically matches strings to make a decision if the method invocation is allowed or not.
private boolean hasAnyAuthorityName(String prefix, String... roles) { Set<String> roleSet = getAuthoritySet(); for (String role : roles) { String defaultedRole = getRoleWithDefaultPrefix(prefix, role); if (roleSet.contains(defaultedRole)) { return true; } } return false; }
5. But how does this all start?
Well, it is all configured in PrePostMethodSecurityConfiguration. In particular in these 2 methods:
These two methods configure:
- the
PreAuthorizeAuthorizationManager
to use DefaultMethodSecurityExpressionHandler - the AuthorizationManagerBeforeMethodInterceptor to use PreAuthorizeAuthorizationManager
And we all know how this configuration is enabled:
@EnableGlobalMethodSecurity(prePostEnabled = true)
Well, let us have a look at AuthorizationManagerBeforeMethodInterceptor.attemptAuthorization(MethodInvocation mi)
which delegates to the configured PreAuthorizeAuthorizationManager to get an AuthorizationDecision and if the decision says the method invocation is not allowed for this Authentication object, then an AccessDeniedException is thrown.
How to implement type-save authorities?
What do we want to achieve?
- A class
enum
Authorities that lists all the authorities required by the application - A custom annotation
@HasAuthority
that will be executed before an annotated method. It accepts only Authorities constants. - The method is allowed if the Authentication object associated with the request has the required
GrantedAuthority
or
- An
AccessDeniedException
is thrown if the Authentication object does not have the requiredGrantedAuthority
.
How are we going to achieve it?
- Option 1: Integrate with Spring Security and replace
@PreAuthorize
with@HasAuthority
- Define custom AuthorizationManager<MethodInvocation> which is based on
@HasAuthority
- Replace Spring default PreAuthorizeAuthorizationManager
- Option 2: Use both Spring Security
@PreAuthorize
and our own custom annotation@HasAuthority
heterogeneously
- Use Spring AOP to define custom
@Aspect
- Register this aspect to handle our custom annotation
“I do not like reading lengthy blog posts. Show me the code!”
Here you go:
- Option 1 – Replace
@PreAuthorize
with@HasAuthority
.
- Option 2 – Co-existing
@PreAuthorize
and@HasAuthority
.
Otherwise, a step-by-step implementation follows with the appropriate explanations.
The Authorities enumeration:
public enum Authority { READ("my.service/read"), WRITE("my.service/write"); private final String value; Authority(final String value) { this.value = value; } public String getValue() { return value; } }
The value field represents the String that the GrantedAuthority holds. Usually, this is an item in the scope claim of an OAuth2/OIDC JWT token. The scope claim usually holds a list of values provided in the scope request parameter of the authorization request.
The @HasAuthority
custom annotation:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface HasAuthority { Authority value(); }
First of all – the AuthorizationAnnotationUtils:
There is a very useful class in Spring Security called AuthorizationAnnotationUtils. Unfortunately, it has a package private access modifier, though there is nothing to be hidden and it is well- documented and tested. It provides very useful functionality for fetching security annotations and throwing an exception if there is more than one annotation of the same type found or some other ambiguity. So, let’s not reinvent the wheel and just copy-paste these three methods in our HasAuthorityAspect
:
- findUniqueAnnotation(Method method, Class<A> annotationType)
- findUniqueAnnotation(Class<?> type, Class<A> annotationType)
- hasDuplicate(MergedAnnotations mergedAnnotations, Class<A> annotationType)
Of course, one can use only the publicly available AnnotationUtils but I like the enhancements on top of it provided by the AuthorizationAnnotationUtils.
How do we fetch the @HasAuthority
value?
The same way the Spring Framework does it (it is an open-source and free software) with the following 2-line method:
but reworked to fetch our custom @HasAuthority:
private HasAuthority findHasAuthorityAnnotation(final Method method) { final HasAuthority hasAuthority = annotationUtils.findUniqueAnnotation(method, HasAuthority.class); return hasAuthority != null ? hasAuthority : annotationUtils.findUniqueAnnotation(method.getDeclaringClass(), HasAuthority.class); }
Where annotationUtils is of type AuthorizationAnnotationUtils (our accessible copy)
Option 1: How @HasAuthority
is going to work?
Define custom AuthorizationManager<MethodInvocation>
Let us look at the documentation of the AuthorizationManager interface:
“An Authorization manager which can determine if an Authentication has access to a specific object.”
It looks exactly like what we need. So, let us implement it. It is a @FunctionalInterface
and there is only one method to implement:
@Override public AuthorizationDecision check(final Supplier<Authentication> authentication, final MethodInvocation object) { final Collection<String> grantedAuthorities = Optional.ofNullable(authentication) .map(Supplier::get) .map(Authentication::getAuthorities) .filter(Objects::nonNull) .orElse(Collections.emptySet()) .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toSet()); final HasAuthority hasAuthorityAnnotation = findHasAuthorityAnnotation(object.getMethod()); final boolean granted = grantAccess(grantedAuthorities, hasAuthorityAnnotation); return new AuthorizationDecision(granted); }
The actual decision is taken by the grantAccess method:
private boolean grantAccess(final Collection<String> grantedAuthorities, final HasAuthority hasAuthority) { return grantedAuthorities.contains(hasAuthority.getValue()); }
Replace Spring default PreAuthorizeAuthorizationManager
Let us look at how is the original PreAuthorizeAuthorizationManager
initialized. It is done in a static method of AuthorizationManagerBeforeMethodInterceptor
, which initializes both the authorization manager and the interceptor, configuring them to work with the @PreAuthorize
annotation.
So, to replace the default PreAuthorizeAuthorizationManager
, we need to tell Spring how to initialize the AuthorizationManagerBeforeMethodInterceptor
. We must define a ROLE_INFRASTRUCTURE @Bean.
@Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public Advisor preAuthorize( final HasAuthorityAuthorizationManager authorizationManager, @Qualifier(HAS_AUTHORITY_ANNOTATION_POINTCUT) final Pointcut pointcut) { final AuthorizationManagerBeforeMethodInterceptor customInterceptor = new AuthorizationManagerBeforeMethodInterceptor(pointcut, authorizationManager); customInterceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder()); return customInterceptor; }
Wait, where did this pointcut come from? If we get back to the default configuration of AuthorizationManagerBeforeMethodInterceptor, we will notice the following line, setting it up to work with the @PreAuthorize annotation:
AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class), authorizationManager)
This returns a pointcut. So, we need the same thing but for our own custom @HasAuthority annotation
. And this is how we do it:
@Bean(HAS_AUTHORITY_ANNOTATION_POINTCUT) Pointcut hasAuthorityPointCut() { return Pointcuts.union( new AnnotationMatchingPointcut(null, HasAuthority.class, true), new AnnotationMatchingPointcut(HasAuthority.class, true)); }
These two @Bean
definitions do the job of replacing a AuthorizationManagerBeforeMethodInterceptor
configured to work with the @PreAuthorize
annotation with a AuthorizationManagerBeforeMethodInterceptor
configured to work with the @HasAuthority
annotation.
Option 2: How is @HasAuthority
going to work?
We need to build a custom @Aspect in order to compare the required authorities against the GrantedAuthorities provided by the Authentication object.
Step-by-step. First of all, the structure of the aspect:
@Aspect @Component public class HasAuthorityAspect { @Before("@annotation(com.egelev.blog.spring.springmethodsecurityenum.security.HasAuthority)") public void hasAuthorityCheck(final JoinPoint joinPoint) throws Throwable { // fetch the Authentication object // extract its GrantedAuthority collection // Fetch the value of the annotation @HasAuthority // check if it is part of the GrantedAuthority collection } }
Note that we use the @Before annotation to build our Advice. This means that our logic is going to be executed only before the target method invocation and it does not have the responsibility to do anything after the invocation is complete.
Here we use the special ‘@annotation()’ Pointcut designator to describe our pointcut expression, which must match all methods annotated with our custom @HasAuthority
annotation.
How do we fetch the Authentication object?
Let’s steal some code from the Spring Security itself – the package private static AUTHENTICATION_SUPPLIER inside AuthorizationManagerBeforeMethodInterceptor
class:
tatic final Supplier<Authentication> AUTHENTICATION_SUPPLIER = () -> { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { throw new AuthenticationCredentialsNotFoundException( "An Authentication object was not found in the SecurityContext"); } return authentication; };
This approach – extracting the Authentication object from the thread- local context of the SecurityContextHolder is widely used in the ‘servlet’ type of applications. Note this is not the way to go in a ‘reactive’ type of application (more on this here).
Once we have the Authentication object, it is easy to collect the String names of its GrantedAuthorities. If there are no granted authorities but if @HasAuthority
is present on the method then the request should not be authorized. As we saw earlier, this means throwing an AccessDeniedException:
final Collection<? extends GrantedAuthority> authenticationAuthorities = Optional.ofNullable(AUTHENTICATION_SUPPLIER) .map(Supplier::get) .map(Authentication::getAuthorities) .filter(Objects::nonNull) .filter(authorities -> !authorities.isEmpty()) .orElseThrow(() -> new AccessDeniedException("Access Denied")); final Set<String> grantedAuthorities = authenticationAuthorities.stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toSet());
So, our HasAuthorityAspect looks like this:
@Aspect @Component public class HasAuthorityAspect { static final Supplier<Authentication> AUTHENTICATION_SUPPLIER = () -> { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { throw new AuthenticationCredentialsNotFoundException( "An Authentication object was not found in the SecurityContext"); } return authentication; }; @Before("@annotation(com.helecloud.blog.security.HasAuthority)") public void hasAuthorityCheck(final JoinPoint joinPoint) throws Throwable { final Collection<? extends GrantedAuthority> authenticationAuthorities = Optional.ofNullable(AUTHENTICATION_SUPPLIER) .map(Supplier::get) .map(Authentication::getAuthorities) .filter(Objects::nonNull) .filter(authorities -> !authorities.isEmpty()) .orElseThrow(() -> new AccessDeniedException("Access Denied")); final Set<String> grantedAuthorities = authenticationAuthorities.stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toSet()); final Method method = MethodSignature.class.cast(joinPoint.getSignature()).getMethod(); final HasAuthority hasAuthorityAnnotation = findHasAuthorityAnnotation(method); String requiredAuthority = Optional.ofNullable(hasAuthorityAnnotation.value()) .map(Authority::getValue) .orElseThrow(() -> new AccessDeniedException("Access Denied")); if (!grantedAuthorities.contains(requiredAuthority)) { throw new AccessDeniedException("Access Denied"); } } }
How to use the custom annotation?
@RestController public class DemoController { @HasAuthority(Authority.READ) @GetMapping("/") public String get() { return "Successful GET request"; } }
It is useful for debugging purposes to ask Spring to provide you with the Authentication object associated with the incoming request by declaring this extra method parameter like so: get(@AuthenticationPrincipal Authentication
authentication)
Some nice but non-essential enhancements
All of the code samples are uploaded here. Some extra features are developed:
@HasAuthority(hasAny={})–
option to list many Authorities, the annotated method invocation will proceed if any item from the hasAny authorities list is available as a GrantedAuthority in the Authentication object@HasAuthority(hasAll={})–
option to list many Authorities, the annotated method invocation will proceed if and only if all the items from the hasAll authorities list are available as a GrantedAuthorities in the Authentication object
Conclusion
This blog proves that with just a few steps we can get rid of the SpEL expressions in the authorization annotations. It is not something that will significantly improve the app’s security. It does not add functionality but it leverages the development process. It makes code reviews easier and decreases the chance of making errors on the endpoints’ permissions. It also enables developers to take advantage of the IDE’s type-safe features like autocompletion and validation.