16 minutes read

In order to ensure the security of your app resources, it is important to grant access only to authenticated users with the necessary permissions. This is where authorization comes into play.

To begin with, keep in mind that authorization always takes place after authentication. Once a client is authenticated, the system determines whether they have permission to access the requested resource.

In this topic, you will learn how to configure authorization in Spring Security. We will guide you through the process of creating a program with multiple endpoints, setting up access rules, and testing the program using Postman.

Note that this topic covers Spring Security 6.1.0

Roles and authorities

The main aim of authorization is to grant access to specific app resources to some authenticated users and restrict access to others. For this, it is important to have a mechanism for distinguishing between users. Spring Security offers the concepts of authorities and roles to help with this task.

roles and authorities diagram

  • Authorities represent the actions that users can perform within your app. They help you control which actions are possible for a user within your app. For example, Jessica may only be able to make READ and WRITE requests to certain endpoints, while Joseph can make READ, WRITE, DELETE, and UPDATE requests to those endpoints. Internally, an authority is simply a string, and we can choose any names for authorities when developing an application.

  • Roles, on the other hand, are groups of authorities. For instance, you may define two types of users in your application: one type may only be able to read and write data, while the other type may be able to read, write, update, and delete data. Instead of using four authorities, you can simply define two roles, such as ROLE_USER and ROLE_ADMIN.

In Spring Security, roles and authorities are often used interchangeably. Behind the scenes, roles are simply authorities with the prefix "ROLE_". The examples in the following sections will explain the difference.

Our program will have several endpoints. Some are available without authentication and others that require authentication and specific roles or authorities:

  • GET /, GET /public — available without authentication;

  • /secured — only available to authenticated users;

  • /user — only available to authenticated users with the USER or ADMIN role;

  • /admin — only available to authenticated users with the ADMIN role;

  • POST /public — only available to authenticated users with the WRITE authority.

Let's begin implementing the program by creating a REST controller and adding a few users with roles. After that, we will proceed with configuring authorization.

Initial setup

Assuming that you've already created a new Spring Boot project which includes the web and security starters, let's add a controller that can handle requests to all the endpoints:

Java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @PostMapping(path = "/public")
    public String postPublic() {
        return "Access to 'POST /public' granted";
    }

    @GetMapping(path = "/public")
    public String getPublic() {
        return "Access to 'GET /public' granted";
    }

    @GetMapping(path = "/secured")
    public String secured() {
        return "Access to '/secured' granted";
    }

    @GetMapping(path = "/user")
    public String user() {
        return "Access to '/user' granted";
    }

    @GetMapping(path = "/admin")
    public String admin() {
        return "Access to '/admin' granted";
    }
}
Kotlin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class DemoController {

    @PostMapping(path = ["/public"])
    fun postPublic(): String = "Access to 'POST /public' granted"

    @GetMapping(path = ["/public"])
    fun getPublic(): String = "Access to 'GET /public' granted"

    @GetMapping(path = ["/secured"])
    fun secured(): String = "Access to '/secured' granted"

    @GetMapping(path = ["/user"])
    fun user(): String = "Access to '/user' granted"

    @GetMapping(path = ["/admin"])
    fun admin(): String = "Access to '/admin' granted"
}

Now let's create some users and assign roles and authorities to them. We'll need three different users, each with the following roles and authorities: the WRITE authority but no role, the USER role, the ADMIN role and the WRITE authority. For now, you can simply hardcode the users in memory. But keep in mind that authorization works the same regardless of whether the users are stored in memory, in a database, or somewhere else.

To assign a role or authority to a user, we'll use the roles and authorities methods, respectively. The roles method takes in one or more role names as strings, while the authorities method takes in one or more authority names as strings.

Here are our hardcoded users:

Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class SecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        var user1 = User.withUsername("user1")
                .password(passwordEncoder().encode("pass1"))
                .authorities("WRITE")
                .build();
        var user2 = User.withUsername("user2")
                .password(passwordEncoder().encode("pass2"))
                .roles("USER")
                .build();
        var user3 = User.withUsername("user3")
                .password(passwordEncoder().encode("pass3"))
                .authorities("ROLE_ADMIN", "WRITE")
                .build();

        return new InMemoryUserDetailsManager(user1, user2, user3);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
Kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.provisioning.InMemoryUserDetailsManager

@Configuration
class SecurityConfig {

    @Bean
    fun userDetailsService(): UserDetailsService {
        val user1 = User.withUsername("user1")
            .password(passwordEncoder().encode("pass1"))
            .authorities("WRITE")
            .build()
        val user2 = User.withUsername("user2")
            .password(passwordEncoder().encode("pass2"))
            .roles("USER")
            .build()
        val user3 = User.withUsername("user3")
            .password(passwordEncoder().encode("pass3"))
            .authorities("ROLE_ADMIN", "WRITE")
            .build()

        return InMemoryUserDetailsManager(user1, user2, user3)
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
}

When defining roles using the roles method, you don't need to add the ROLE_ prefix because Spring Security will automatically add it. However, when defining a role using the authorities method, you must prefix it with ROLE_. For example, roles("ADMIN") is equivalent to authorities("ROLE_ADMIN"). It is important to keep this in mind to avoid confusion and mistakes.

Remember that authorities are case-sensitive strings!

Our program will also have a simple index.html file in the /resources/static folder with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Home</title>
</head>
<body>
    <h1>Welcome!</h1>
    <p>This page is available to anyone</p>
</body>
</html>

Now that we've created some users with roles and authorities, it's time to move on to defining authorization. In the next section, we'll introduce some useful methods for configuring access rules.

Request matchers

In this demo application, we will enable Basic authentication for users. To do this, we'll use an HttpSecurity object to build the required SecurityFilterChain bean. Invoking the authorizeHttpRequests method, you can configure access to different endpoints. Here's an example configuration that makes all endpoints available only to authenticated users:

Java
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                .httpBasic(Customizer.withDefaults())
                .build();
    }

    // UserDetailsService bean
}
Kotlin
@Configuration
class SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
        http
            .authorizeHttpRequests { auth ->
                auth.anyRequest().authenticated()
            }
            .httpBasic(Customizer.withDefaults())
            .build()

    // UserDetailsService bean
}

However, you need to apply more granular access control so that only users with certain authorities can access certain endpoints. This can be done by calling some of the methods of the configurer. The methods can be divided into two groups: one allows specifying endpoints to which you want to configure access, and another allows specifying the users who can access these endpoints.

You select the endpoints using the following methods:

  • requestMatchers(...) is an overloaded method that can accept, optionally, an HTTP method and/or one or more endpoint path patterns. For example, requestMatchers("/secured") matches any requests to the /secured URL, requestMatchers(HttpMethod.PUT, "/product", "/user") matches PUT requests to /product and /user URLs, and requestMatchers(HttpMethod.GET) matches GET requests to any URL.

  • anyRequest() maps any request, regardless of the URL or HTTP method you use.

Once you've selected the endpoints you want to protect, you need to specify who can access them. This can be done using various methods that Spring Security provides. Let's take a closer look at them:

  • permitAll() specifies that anyone can access a URL, whether they're authenticated or not.

  • denyAll() specifies that no one can access a URL.

  • authenticated() specifies that any authenticated user can access a URL.

  • anonymous() this method specifies that only unauthenticated users can access a URL.

  • hasRole(...) this method specifies that a URL is accessible only to authenticated users possessing a particular role. Note that the name of the role should not start with ROLE_ as it's automatically added.

  • hasAnyRole(...) is similar to hasRole(...), but it allows specifying multiple roles.

  • hasAuthority(...) specifies that the user must have a specific authority to access the endpoint. For example, hasAuthority("READ") requires that the user has the READ authority. You can also use this method to specify a role, but it should be prefixed with ROLE_. For instance, hasAuthority("ROLE_ADMIN") is similar to hasRole("ADMIN").

  • hasAnyAuthority(...) is similar to hasAuthority(...), but it allows specifying multiple authorities.

Additionally, you can use wildcards in your endpoint path patterns to match multiple URLs. The patterns provided to request matchers support the following wildcards:

  • ? matches one character. For example, requestMatchers("/?") matches /a and /b but not / or /ab.

  • * matches zero or more characters. For example, requestMatchers("/*") matches /, /abc, and /defg but not /ab/cd.

  • ** matches zero or more directories in a path. For example, requestMatchers("/**") matches /, /ab, /cd, and /a/b/c.

You can place wildcards at any point of the path to match any possible URL. Here are some examples: /page/?, /page/*/comments, /api/**.

By combining these methods and wildcards, you can create complex rules for granular access control over your endpoints. Now that you know how to configure authorization in Spring Security, let's put the pieces together and finish the application.

Configuring authorization

Now that you have a basic SecurityFilterChain bean set up, let's configure some authorization rules using the methods from the previous sections. To configure access to your endpoints, you need to chain together the appropriate method calls. Let's take a look at an example of how you can make the /admin endpoint available only to users with the ROLE_ADMIN role:

.requestMatchers("/admin").hasRole("ADMIN")

Here, the hasRole method specifies that only users with the ROLE_ADMIN role can access the /admin endpoint. If you want to add more access rules, you can chain more method pairs together. Here's an example of the full set of rules:

Java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
            .authorizeHttpRequests(matcherRegistry -> matcherRegistry
                    .requestMatchers("/user").hasAnyRole("USER", "ADMIN")
                    .requestMatchers("/admin").hasRole("ADMIN")
                    .requestMatchers(HttpMethod.POST, "/public").hasAuthority("WRITE")
                    .requestMatchers("/secured").authenticated()
                    .requestMatchers(HttpMethod.GET, "/*").permitAll()
                    .anyRequest().denyAll()
            )
            .httpBasic(Customizer.withDefaults())
            .csrf(csrfConfigurer -> csrfConfigurer.disable())  // for POST requests via Postman
            .build();
}
Kotlin
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
    http
        .authorizeHttpRequests { matcherRegistry -> matcherRegistry
            .requestMatchers("/user").hasAnyRole("USER", "ADMIN")
            .requestMatchers("/admin").hasRole("ADMIN")
            .requestMatchers(HttpMethod.POST, "/public").hasAuthority("WRITE")
            .requestMatchers("/secured").authenticated()
            .requestMatchers(HttpMethod.GET, "/*").permitAll()
            .anyRequest().denyAll()
        }
        .httpBasic(Customizer.withDefaults())
        .csrf { it.disable() }  // for POST requests via Postman
        .build()

Let's break this down line by line. The first matcher configures the /user endpoint to be accessible for authenticated users with the ROLE_USER or ROLE_ADMIN role. The second matcher grants access to the /admin endpoint only to authenticated users with the ROLE_ADMIN role. The next matcher allows only users with the WRITE authority to make POST requests to the /public endpoint. The /secured URL is made accessible to any authenticated users. Notice that in the next line, the "/*" wildcard is used to define both the / and /public endpoints to allow any user, whether authenticated or not, to make GET requests to these endpoints. Finally, the last matcher denies any requests access to any other endpoint.

It's important to note that the order of the matchers matters. If any endpoint matches the patterns of multiple matchers, the first one in the chain will apply its access rules to the request. That's why we put .requestMatchers("/secured").authenticated() before .requestMatchers(HttpMethod.GET, "/*").permitAll(). Otherwise, since the "/*" wildcard matches /secured too, GET requests to the /secured URL would be available to any user, which is not what we want to achieve. Pay attention to this when configuring authorization. The order of request matcher patterns must be from specific to general.

In this example, we've also enabled HTTP basic authentication. You can now use this type of auth and Postman to test your program.

It's worth mentioning that Spring Security enables CSRF (Cross-Site Request Forgery) protection by default. If you try to send a POST request using Postman or a similar program to your application, you will receive the 403 Forbidden status code because of this protection. When testing POST requests, you can disable this type of protection by calling the .csrf(cfg -> cfg.disable()) method on the HttpSecurity object. In one of future topics, you will learn more about configuring CSRF protection.

Running the app

Now that your program is set up, let's run and test it out. Start by trying to access the root URL, which must be accessible to anyone with no authentication:

spring security postman get request

As expected, you receive a 200 OK status code and see the content of index.html. Similarly, /public is also accessible.

However, you won't be able to access any of the other URLs without authentication. They can only be accessed by authenticated users. Here's the response you get when sending a request to /secured (or any other endpoint that requires authentication) without authentication:

spring security postman unauthorized status code

The request is rejected with a 401 Unauthorized status code, indicating that the request requires user authentication. To fix this error, you need to use HTTP basic authentication and input a valid login/password pair. You can do this in the "Authorization" tab in Postman. Let's use the login and password of the first user who doesn't have any role and try again.

spring security postman basic authentication

Note that besides the login and password, we also specified the type of authentication as "Basic Auth". As you can see, it is now possible to access the /secured endpoint and receive a response with a 200 OK status code. If you try to access /secured using the credentials of the other two users, you will get the same response.

However, what happens if you try to access /user or /admin as a user who doesn't have the required role? Let's try to access /admin as the first user who doesn't have the ROLE_ADMIN role:

spring security postman forbidden status code

Even when you provide a valid login/password pair and successfully authenticated, you will receive a 403 Forbidden status code. This means that the server did not authorize the user to access the requested endpoint. Only users with the ROLE_ADMIN role are allowed to access /admin. So the application works as intended.

Feel free to experiment with different users and endpoints and test out how the other access rules work.

Conclusion

In this topic, you learned how to configure authorization rules in a Spring Security application invoking the authorizeHttpRequests method of the HttpSecurity object. You saw how to chain together method calls to specify which endpoints are accessible to which roles or authorities, and how to enable HTTP basic authentication. You also learned about the order of the request matchers and how it affects the authorization rules. Finally, you saw how to test an application using Postman and how to handle CSRF protection when testing POST requests. By following these principles, you can create secure and reliable applications that restrict access to sensitive resources and provide an additional layer of security to its users.

50 learners liked this piece of theory. 2 didn't like it. What about you?
Report a typo