Declarative authorization with Spring-Security @Secured annotation

Ravi Chauhan
6 min readJul 7, 2020

--

Authors: Ravi Chauhan & Agarwalshubhi

Prerequisites

  1. Basic understanding of spring-security framework
  2. Worked on Spring-boot applications

Introduction

Spring Security is one of the most advanced frameworks available today that enables developers to enforce authorization at a method-level and type-level as mentioned in the Secured Interface below:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Secured {
String[] value();
}

@Secured annotations work well when you need to check the privileges like role & permissions. But when we ask a bigger question that, is @Secured annotation self-sufficient to perform fine-grained authorization with the current implementation considering the dynamic nature of method invocations based on various arguments?

The answer to this is, maybe not. But why?

With the growing scale in today’s era and humongous scale, just authentication along with role & permissions-based authorization is not sufficient enough. There is a rapid need arising for finer access control. This finer access control can have any engine running behind the scenes to check access control. But, can we have a simpler and easier way to look at it? Can we use the existing features to make it future-ready? Yes, we can.

Now the next question is, can we achieve this using the already existing spring security paradigm?

Below code-snippet shows how this @Secured annotation is used in the code for authorization purpose. This is also called as authorization based on pre-invocation handling. A pre-invocation decision on whether the invocation is allowed to proceed further is made by the “AccessDecisionManager”

#1 
@Secured ({"ROLE_USER", "ROLE_ADMIN"})

public void addUser(String name, String pwd);
OR #2
@Secured(["{key1:value1, key2:value2}"]) // any format which we want to parse

Note that, in the above code snippet, you see 2 ways of passing arguments to @Secured. Are both of these supported by spring-security out of the box? No, it is not.

The first one comes handy where you can pass roles or permissions. But, in the second one, you see a JSON. Can that be parsed by not making any other changes or extended implementations? No, it is not possible.

Therefore, the below section describes the changes required to make it possible and parse any Array of JSON/String that we want which comes as input from the @Secured annotation argument. Please note, the arguments which are accepted are in the format of String array as you can see below:

public @interface Secured {
String[] value();
}

There are 2 other annotations provided by spring-security, where you can also give SpEL expressions in the parameters which is not possible in case of @Secured annotation.

@PreAuthorize("isAuthenticated()")
public String unlock() {
return "you are inside the locker";
}
@PostAuthorize("returnObject.ownerName == authentication.name")
public Owner getOwner();
Note*: Spring-security provides built-in keyword i.e. returnObject

Architecture

To make the implementation more clear, you should have some understanding of what you are trying to do here. Therefore, the basic authorization architecture in spring-security is shown in the below diagram which also highlights where your custom code fits in.

Below diagram depicts the workflow of how custom authorization can be performed in spring with @Secured annotation.

Decision Manager workflow

If you look closely, “CustomDecisionVoter” is our custom implementation for fetching the arguments from @Secured annotation and process them to provide the right vote to access or deny the respective invocation.

Implementation

To start with the implementation, you need to have the custom implementation of “GlobalMethodSecurityConfiguration”. This is required to register your “CustomDecisionVoter” in the application container’s “AccessDecisionManager”. Also note, you can choose the desired implementation for managing decisions i.e. one of “AffirmativeBased” OR “ConsensusBased” OR “UnanimousBased”. You can find more details on this under spring documentation here.

@EnableGlobalMethodSecurity(
securedEnabled = true
)
public class CustomMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private CustomDecisionVoter customDecisionVoter;

public MethodSecurityConfig() {
}

@Bean
protected AffirmativeBased accessDecisionManager() {
List<AccessDecisionVoter<?>> voters = new ArrayList();
//you can register as many voters as you want
voters.add(this.customDecisionVoter);
AffirmativeBased decisionManager = new AffirmativeBased(voters);
decisionManager.setAllowIfAllAbstainDecisions(false);
return decisionManager;
}
}

Note that, the flag secureEnabled, the value is set to true so that you can use the @Secured annotation of spring-security. To enable the @PreAuthorize & @PostAuthorize, there is a different flag as shown below:

@EnableGlobalMethodSecurity(
prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {}
  • prePostEnabled property enables Spring Security pre/post annotations.
  • securedEnabled property determines if the @Secured annotation should be enabled.

Sometimes, it might so happen that your application is already implementing the custom “WebSecurityConfigurerAdapter”. So in that case, you do not need to add it again else you will start facing issues. The reason behind that is, theWebSecurityConfigurerAdapter” is already annotated with @order(100) and @Order annotation on each static class indicates the order in which the configurations will be considered to find one that matches the requested URL.

Note that, the order value for each class must be unique.

Below code-snippet can be used just in case you wanted to define multiple entry-points. Just ensure that you are adding the order appropriately.

@Configuration
@ConditionalOnMissingBean({WebSecurityConfigurerAdapter.class})
@Order(Ordered.LOWEST_PRECEDENCE)
public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
public CustomWebSecurityConfigurerAdapter() {
}

protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.formLogin().disable();
httpSecurity.csrf().disable();
}
}

So you are all set so far with enabling “CustomDecisionVoter” but not written the logic for it. The below code snippet will help you do that.

@Component
public class CustomDecisionVoter implements AccessDecisionVoter<Object> {
....

public boolean supports(ConfigAttribute configAttribute) {
return true;
}

public boolean supports(Class<?> aClass) {
return true;
}

public int vote(Authentication authentication, Object securedObject, Collection<ConfigAttribute> methodConfigs) {

// custom implementation goes here
...... //return 0/1; }

Focus on the vote function in the above snippet, you can get your arguments passed to @Secured annotation from the methodConfigs argument shown below is an example:

Object[] args = methodConfigs.toArray();
for(Object object : args){
// process the object or convert to any Java Domain form
}

The vote function returns either a ‘0’ or ‘1’ which means “AccessDenied” or “AccessGranted”. In case of “AccessDenied”, you need to handle the exception in your ExceptionHandler for returning appropriate response or spring will throw the default exception “AccessDenied” and in case of “AccessGranted” it will be a pass-through and the invocation will happen seamlessly.

Extensions

Great! we had done our implementation for using @Secured in our own customized way of passing arguments and using them to return the respective decisions.

The next section now describes the way we can also create our own annotations and add them to method arguments level or method level to get a better control of the way we want to pass the arguments for getting the decision based on specific inputs instead of reading through all the method arguments.

@Secured(["k1:v1, k2:v2"])
public void securedMethod(@AuthorizationInput(name="input1") String securityParameter){
//Method implementation goes here...
....
}

What are we doing in above snippet is, we are creating our own annotations for method argument level. For example:

@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthorizationInput {
String name() default "input";
String value() default "test";
}

Now, along with the parameters that have been defined in the @Secured annotation, we can write some reflection code to read through all the annotated fields as we want and use those as parameters to get the final decision instead of reading through all the arguments and having a performance hit for processing. You can read specific annotated fields with this approach. Also, you can read about @Repeatable annotations to do effective processing of your inputs annotated by custom annotations.

For example:

@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(AuthorizationInputs.class)
public @interface AuthorizationInput {
String name() default "";

String value() default "";
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthorizationInputs {
AuthorizationInput[] value();
}

Find below how you can pick up the custom annotated arguments in your vote method:

public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> methodConfigs) {   // @Secured argument can be fetched 
// from methodConfigs as mentioned above
try {
MethodInvocation mi = (MethodInvocation)object;
Parameter[] params = mi.getMethod().getParameters();
Object[] arguments = mi.getArguments();
for(int i = 0; i < params.length; ++i) {
Parameter param = params[i];
....
}
} catch (Exception e) {
e.printStackTrace();
}

// decision making implementation
return 0/1;
}

That’s it. This type of implementation gives you much finer control over what can be the inputs and how you can make decisions based on inputs and get to an authorization decision.

Summary and learning

  1. Using existing spring-security filter chain helps in doing existing + custom authorization in a much more powerful way.
  2. A lot of boilerplate code is avoided.
  3. You have full control on the input you want to pass as arguments to @Secured. “k1:v1, k2:v2” is just an example for demonstration.
  4. Custom annotations for method/parameter/field level gave much better flexibility and control on the passed input.
  5. You decide on any number of DecisionVoters’s you want to implement.
  6. Clean code, easy to understand and is extensible.

Finally, you are all set to test your declarative authorization based on spring-security annotation.

--

--