×
☰ See All Chapters

Endpoint and HTTP Method-based Access Control with Spring Security

In this chapter, we delve into applying authorization constraints to specific groups of requests within a Spring Security-enabled application. Imagine a scenario where we need to expose two endpoints: /admin and /user. Our goal is to ensure that only users with the ADMIN role can access the /admin endpoint, while allowing anyone to access the /user endpoint.

Spring Security offers three types of matcher methods to facilitate this restriction:

  1. MVC Matchers: These utilize MVC expressions for path selection. 

  2. Ant Matchers: These employ Ant expressions for path selection. 

  3. Regex Matchers: These utilize regular expressions (regex) for path selection. 

MVC Matchers

Two primary methods are available for declaring MVC matchers:

mvcMatchers(HttpMethod method, String... patterns): Allows specification of both the HTTP method and paths to which restrictions apply. Ideal when different restrictions are needed for different HTTP methods for the same path.

mvcMatchers(String... patterns): Simplifies usage when applying authorization restrictions based solely on paths. Automatically applies restrictions to any HTTP method used with the path.

Let's explore mvcMatchers through examples:

Example 1

 We need to expose two endpoints: /admin and /user. We want to ensure that only users with the ADMIN role can access the /admin endpoint, while anyone can access the /user endpoint.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
@Override
   
protected void configure(HttpSecurity http) throws Exception {
       http.httpBasic();
       http.authorizeRequests()
               .mvcMatchers(
"/admin").hasRole("ADMIN")
               .mvcMatchers(
"/user").hasAnyRole("MANAGER", "ADMIN");
   }
}

 

In the above configuration, if you add a new endpoint to your application or any endpoint that is left out without setting a rule in ApplicationWebSecurityConfigurerAdapter, it becomes accessible by default to anyone, including unauthenticated users. This is the default behavior, which is the same as .anyRequest().permitAll() as shown below:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
@Override
   
protected void configure(HttpSecurity http) throws Exception {
       http.httpBasic();
       http.authorizeRequests()
               .mvcMatchers(
"/admin").hasRole("ADMIN")
               .mvcMatchers(
"/user").hasAnyRole("MANAGER", "ADMIN")
               .anyRequest().permitAll();
   }
}

 

When invoking a new endpoint without credentials, you receive a success response. Similarly, when invoking it with correct credentials, you also receive a success response. However, if incorrect or bad credentials are provided, a 401 "Unauthorized" response is returned. This behavior may seem peculiar, but it ensures that the framework evaluates any username and password provided in the request.

You could opt to make all other endpoints accessible only for authenticated users. To achieve this, you would replace the permitAll() method with authenticated() as presented in the following listing. Likewise, you could even deny all other requests by using the denyAll() method.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
@Override
   
protected void configure(HttpSecurity http) throws Exception {
       http.httpBasic();
       http.authorizeRequests()
               .mvcMatchers(
"/admin").hasRole("ADMIN")
               .mvcMatchers(
"/user").hasAnyRole("MANAGER", "ADMIN")
               
.anyRequest().authenticated();
   }
}

 

When using matchers to refer to requests, the order of the rules should be from specific to general. This is why the anyRequest() method cannot be called before a more specific matcher method like mvcMatchers(). In the code snippet below, all requests, including /admin, are accessible by anyone, even though we have set /admin to be accessible only by users with the ADMIN role.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
@Override
   
protected void configure(HttpSecurity http) throws Exception {
       http.httpBasic();
       http.authorizeRequests()
               
.anyRequest().permitAll()
               .mvcMatchers(
"/admin").hasRole("ADMIN")
               .mvcMatchers(
"/user").hasAnyRole("MANAGER", "ADMIN");
   }
}

 

Example 2

Let's ensure that all endpoints starting with /admin require authentication. For instance, admin/updateDetails and admin/createUser should only be accessible to authenticated users with the ADMIN role.

To accomplish this, we utilize the ** operator. In Spring MVC, this operator allows us to match any number of pathnames.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
@Override
   
protected void configure(HttpSecurity http) throws Exception {
       http.httpBasic();
       http.authorizeRequests()
               
.mvcMatchers("/admin/**").hasRole("ADMIN")
               .mvcMatchers(
"/user").hasAnyRole("MANAGER", "ADMIN")
               
.mvcMatchers( "/x/**/z") .authenticated();
   }
}

 

The ** operator matches multiple pathnames. For instance, /admin/** would match /admin, /admin/updateDetails, and /admin/createUser. Similarly, /x/**/z would match paths like /x/y/z and /x/a/b/z. If you want to match only one pathname, you can use a single * . For example, a/*/c would match a/b/c and a/d/c, but not a/b/d/c.

These are common expressions used for path matching with MVC matchers:

  • /x: Matches only the path /x. 

  • /x/*: The * operator replaces one pathname. For instance, it matches /x/y or /x/z, but not /x/y/z. 

  • /x/**: The ** operator replaces multiple pathnames. It matches x/,  /x/y or /x/z, and /x/y/z 

  • /x/{param}: Applies to the path /x with a given path parameter. 

  • /x/{param:regex}: Applies to the path /x with a given path parameter only when the value of the parameter matches the given regular expression. 

Example 3

We have an endpoint with a path variable, and we want to deny all requests that use a value for the path variable that contains anything other than digits.

In the following example, we are assessing user eligibility to vote by extracting the user's age from the path variable. We reject the request if the provided age is not a number.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
@Override
   
protected void configure(HttpSecurity http) throws Exception {
       http.httpBasic();
       http.authorizeRequests()
               .mvcMatchers(
"/testVotingAge/{age:^[0-9]*$}").permitAll() //Allow only if age is number
               
.anyRequest().authenticated();
   }
}

 

Note: When using parameter expressions with a regex, ensure there are no spaces between the parameter name, the colon (:), and the regex, as demonstrated in the code listing.

Example 4

We have /admin endpoints for both POST and GET methods. Our requirement is to restrict the /admin POST endpoint so that it is accessible only by users with the ADMIN role.

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
@Override
   
protected void configure(HttpSecurity http) throws Exception {
       http.httpBasic();
       http.authorizeRequests()
               .mvcMatchers(
HttpMethod.POST, "/admin").hasRole("ADMIN") // Restrict POST method to ADMIN role
               
.anyRequest().permitAll(); // All other requests are permitted
       
http.csrf().disable(); // CSRF protection is disabled for simplicity in this example
   
}
}

 

Note: Spring Security typically uses CSRF tokens to mitigate CSRF vulnerabilities. We are temporarily disabling CSRF protection (http.csrf().disable()) to simplify the example and focus on matcher methods. However, this is not generally recommended.

ANT Matchers

The three methods available when using Ant matchers are:

antMatchers(HttpMethod method, String patterns): This method allows you to specify both the HTTP method to which the restrictions apply and the Ant patterns that refer to the paths. It's useful if you need to apply different restrictions for different HTTP methods for the same group of paths.

antMatchers(String patterns): This method is simpler and easier to use if you only need to apply authorization restrictions based on paths. The restrictions automatically apply for any HTTP method.

antMatchers(HttpMethod method): This is equivalent to antMatchers(HttpMethod, "/**") and allows you to refer to a specific HTTP method disregarding the paths.

When using MVC matchers in Spring Security to configure security for a specific path, such as /hello, the security rules automatically apply to both the exact path and its sub-paths, like /hello/. This behavior stems from Spring Security leveraging Spring MVC's path matching functionality, where /hello and /hello/ are treated as equivalent paths. Therefore, when you secure /hello, you're effectively securing /hello/, ensuring that access control rules remain consistent across both variations of the path. On the other hand, Ant matchers, which rely on explicit Ant expressions for patterns, do not inherently understand Spring MVC's path conventions. So, if you specify /hello as an Ant expression, it only matches exactly /hello and not /hello/. This means that with Ant matchers, you would need to define separate rules for /hello and /hello/ if you want them to be secured independently.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
@Override
   
protected void configure(HttpSecurity http) throws Exception {
       http.httpBasic();
       http.authorizeRequests()
               .antMatchers(
"/hello/").authenticated()
               .antMatchers(
"/hello").authenticated();
   }
}

 

Regex matchers

You might encounter requirements that are more specific and cannot be addressed solely with Ant and MVC expressions. An example of such a scenario could be: "Deny all requests when paths contain specific symbols or characters." For such cases, you would need to utilize a more powerful expression like a regex.

There are two methods available to implement regex matchers:

regexMatchers(HttpMethod method, String regex): This method allows you to specify both the HTTP method to which restrictions apply and the regex patterns that refer to the paths. It's beneficial when you need to apply different restrictions for different HTTP methods for the same set of paths.

regexMatchers(String regex): This method is simpler and easier to use if you only need to apply authorization restrictions based on paths. The restrictions automatically apply for any HTTP method.

For example, suppose the application receives the country and language in two path variables from where the user makes the request. In this scenario, we consider that any authenticated user can access the content if the request originates from the US, Canada, or the UK, or if English is used.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
@Override
   
protected void configure(HttpSecurity http) throws Exception {
       http.httpBasic();
       http.authorizeRequests()
               .regexMatchers(
".*/(us|uk|ca)+/(en|fr).*")
               .authenticated()
               .anyRequest()
               .hasAuthority(
"premium");
   }
}

 

 


All Chapters
Author