Mastering Authorization and Authentication With Spring Security

Written by sammytran | Published Invalid Date
Tech Story Tags: spring-security | spring-boot | authentication | authorization | programming | coding | java | technology

TLDRAuthentication is the process of proving user identity. Spring Security provides a powerful and flexible framework for implementing authentication and authorization. In this guide, we will learn more about sessions, a typical method of authenticating users over HTTP. We will use Spring Security to secure a sample Spring Boot application.via the TL;DR App

If you are building a REST API with Spring Boot, you will eventually need to secure your application. Luckily, Spring Security provides a powerful and flexible framework for implementing authentication and authorization. We will start by understanding these two ideas conceptually. Afterward, we will use Spring Security to secure a sample Spring Boot application.

Authentication

Authentication is a broad term, but in the context of REST APIs, authentication is the process of proving user identity. Since the early days of the Internet, authentication has been important because the Internet is an open and global platform. Authentication helps applications build trust with their users by ensuring that only authorized individuals can access protected resources.

Over the years, many methods for authenticating users have been created to address security challenges and increasing demand for user convenience and privacy.

Some of the most popular authentication methods include:

  1. Basic Access Authentication – Since its invention in the 1990s, browsers could include a request header like Authorization: Basic <CREDENTIALS>, where the credentials are a Base64 encoded username and password joined by a colon. All HTTP requests accessing protected resources needed to include this request header.

  2. Sessions – With the invention of cookies, browsers could now store user session state, allowing users to log in once and stay authenticated for subsequent requests without re-entering user credentials.

  3. Multi-Factor Authentication (MFA) – In the past decade, having users prove their identity in multiple ways has become popular because it is more secure than using passwords alone.

  4. Single Sign-On (SSO) – In recent years, you may have seen that many platforms now allow you to register for accounts and authenticate using Facebook or Google. This feature is called SSO and has become popular because of its convenience.

In this guide, we will learn more about sessions, a typical method of authenticating users over HTTP. While sessions are a bit outdated compared to other modern authentication methods, they are simple to understand and still have production use cases.

How To Authenticate Users With Sessions

As mentioned in the previous section, sessions are an authentication method that leverages cookies, which you could think of as a key-value pair with optional metadata fields that indicate its expiration time and restrict its access. Clients can send cookies to servers by using the Cookie request header. For example, an HTTP request might contain the following request header:

Cookie: JSESSIONID=B2236C29AC9CBE6FE0DC02A61596554D

Here, the request header includes a cookie named JSESSIONID, the default name of session cookies created by Tomcat, the default servlet container used by Spring Boot. The corresponding value is a unique identifier that Spring Security uses to identify the user making the request.

Clients typically get cookies from servers, which send cookies by using the Set-Cookie response header. For example, a server might respond to an HTTP request with a response containing the following response header:

Set-Cookie: JSESSIONID=B2236C29AC9CBE6FE0DC02A61596554D; Path=/; HttpOnly

Let’s break down the three parts of this response header:

  1. JSESSIONID=B2236C29AC9CBE6FE0DC02A61596554D – This part tells us that the cookie’s name is JSESSIONID and its associated value is B2236C29AC9CBE6FE0DC02A61596554D.

  2. Path=/ – This part tells us that the server intends for this cookie to be included in all requests where the URL path contains the prefix /. Since all URL paths start with /, this tells the client that all subsequent requests need this cookie in the request header.

  3. HttpOnly – This part is a flag that prevents client-side scripts (JavaScript) from accessing this cookie, which helps to mitigate cross-site scripting (XSS) attacks.

Putting it all together, the entire authentication process for a sample application might look like this:

  1. An unauthenticated client sends a POST request to the /login endpoint with the following request body:

    {
      "username": "foo",
      "password": "bar"
    }
    

  2. The server verifies whether the user credentials are valid or not. If they are, then the server creates an in-memory session object uniquely identified by a JSESSIONID, which is returned to the client as a cookie in the response header:

    Set-Cookie: JSESSIONID=B2236C29AC9CBE6FE0DC02A61596554D; Path=/; HttpOnly
    

  3. Subsequent HTTP requests made by the client include the session cookie in the request header:

    Cookie: JSESSIONID=B2236C29AC9CBE6FE0DC02A61596554D
    

  4. The server uses the JSESSIONID in the request to look up the corresponding session object. If it exists, then it means the client has previously authenticated with the server successfully, so the server proceeds with handling the request.

  5. Later, the client sends a GET request to the /logout endpoint to terminate the current session.

  6. The server handles this request by invalidating the session object corresponding to the provided JSESSIONID.

Authorization

Authentication and authorization are two terms that are related but distinct. Authentication is about verifying who you are, while authorization is about what you are allowed to do. For example, an application might have regular users and administrators, who are able to perform actions that regular users should not be allowed to perform (e.g., banning users from the platform).

In the context of sessions, authorization is the responsibility of the server. The server can access each user’s login credentials (username and password) and their role or permission level. When an HTTP request comes in with a JSESSIONID, the server looks up the associated session object and checks whether the authenticated user is authorized to make the request.

Full Code Example

Now that we have the necessary background knowledge, let’s use what we have learned to secure a sample application. First, we must add Spring Security to our Spring Boot project (f you do not already have a minimally bootstrapped Spring Boot project, consider reading my post on Spring Initializr).

If you are using Gradle, add the following line to the dependencies block in the build.gradle file:

dependencies {
  // ...
  
  implementation 'org.springframework.boot:spring-boot-starter-security'
}

If you are using Maven, add the following lines to the <dependencies> element in your pom.xml file:

<dependencies>
  <!-- ... -->
  
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
  </dependency>

</dependencies>

Next, create a new class called ExampleController, which handles GET requests to /, /users, and /admins:

@RestController
public class ExampleController {

  @GetMapping("/")
  public String home() {
    return
        "<html>\n" +
        "  <head>\n" +
        "    <title>Home</title>\n" +
        "    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" +
        "  </head>\n" +
        "  <body>\n" +
        "    <p>\n" +
        "      <a href=\"http://localhost:8080/users\">Users</a>\n" +
        "    </p>\n" +
        "    <p>\n" +
        "      <a href=\"http://localhost:8080/admins\">Admins</a>\n" +
        "    </p>\n" +
        "    <p>\n" +
        "      <a href=\"http://localhost:8080/logout\">Log out</a>\n" +
        "    </p>\n" +
        "  </body>\n" +
        "</html>\n";
  }

  @GetMapping("/users")
  public String getUsers() {
    return "Only users can see this";
  }

  @GetMapping("/admins")
  public String getAdmins() {
    return "Only admins can see this";
  }
}

By adding Spring Security as a dependency, all requests require authentication by default. We can see this by starting the Spring Boot application locally and going to http://localhost:8080/ with a web browser.

We should be redirected to Spring Security’s default login page, http://localhost:8080/login:

Since our Spring Boot application has no users, there is no way to sign in. We can create users by defining a Spring bean that returns a UserDetailsService in a new WebSecurityConfiguration class:

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration {

  @Bean
  public UserDetailsService users() {
    UserDetails user = User.builder()
        .username("user")
        .password("{noop}password")
        .roles("USER")
        .build();
        
    UserDetails admin = User.builder()
        .username("admin")
        .password("{noop}password")
        .roles("ADMIN")
        .build();
        
    return new InMemoryUserDetailsManager(user, admin);
  }
}

In the code block above, we create one regular user and one administrator, both of which use password as their password for simplicity.

We prefix the password with {noop} so Spring Security knows this is a plaintext password. If we wanted to, we could, for instance, use a hashing algorithm like bcrypt:

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration {

  @Bean
  public UserDetailsService users() {
    UserDetails user = User.builder()
        .username("user")
        .password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG")
        .roles("USER")
        .build();
        
    UserDetails admin = User.builder()
        .username("admin")
        .password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG")
        .roles("ADMIN")
        .build();
        
    return new InMemoryUserDetailsManager(user, admin);
  }
}

To learn more about how Spring Security handles password storage, see the official documentation on password storage.

Now, when we go to http://localhost:8080/, we can try signing in as a user by using user as the username and password as the password. We should be able to successfully authenticate and redirect back to http://localhost:8080/, which looks like this:

If we click on the Users link, we should see a greeting for users, and if we go back and click on the Admins link, we will also be able to see the greeting for administrators, even though we authenticated as a user. This is possible because we have not protected /users and /admins by role.

To do that, we can define a Spring bean that returns a SecurityFilterChain in the WebSecurityConfiguration class:

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(
            requests ->
                requests
                    .requestMatchers(HttpMethod.GET, "/users")
                    .hasRole("USER")
                    .requestMatchers(HttpMethod.GET, "/admins")
                    .hasRole("ADMIN")
                    .anyRequest()
                    .authenticated())
        .formLogin(Customizer.withDefaults());
    return http.build();
  }

  @Bean
  public UserDetailsService users() {
    UserDetails user = User.builder()
        .username("user")
        .password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG")
        .roles("USER")
        .build();
        
    UserDetails admin = User.builder()
        .username("admin")
        .password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG")
        .roles("ADMIN")
        .build();
        
    return new InMemoryUserDetailsManager(user, admin);
  }
}

The securityFilterChain method above has the following rules:

  1. All GET requests to /users must come from an authenticated user with the USER role.
  2. All GET requests to /admins must come from an authenticated user with the ADMIN role.
  3. All other requests (except for /login) must come from an authenticated user.

Now, when we go to http://localhost:8080/, log in as a user, and click on the Admins link, we should get a default error page indicating that the Spring Boot server returned a response of 403 Forbidden:

If we log out and log back in as an administrator using admin as the username and password as the password, we should be able to access the Admins page, but not the Users page, as expected.

Conclusion

Congratulations! You now better understand authentication and authorization, how user sessions work, and how to secure REST API endpoints with Spring Security.

Also published here.


Written by sammytran | Software engineer, writer, and foodie based out of Seattle.
Published by HackerNoon on Invalid Date