KnowledgeShop

Learn & Share

Spring Security

Introduction

First Intro Project

  • Spring Boot scans for components only in the package (and its subpackages) that contains the class annotated with @SpringBootApplication. If you annotate classes with any of the stereotype components in Spring outside of the main package, you must explicitly declare the location using the @ComponentScan annotation.
Maven configuration
1
2
3
4
5
6
7
8
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
Web service endpoint
1
2
3
4
5
6
7
8
@RestController
public class HelloController {

  @GetMapping("/hello")
  public String hello() {
    return "Hello!";
  }
}
  • When you run the app, and access the endpoint curl http://localhost:8080/hello, you get the below error
1
2
3
4
5
6
{
  "status":401,
  "error":"Unauthorized",
  "message":"Unauthorized",
  "path":"/hello"
}
  • By default, Spring Security expects the default username (user). Each time you run the application, it generates a new password and prints this password in the console.
  • It works once the generated password is passed in as input

With cURL, you can set the HTTP basic username and password with the -u flag. Behind the scenes, cURL encodes the string <username>:<password> in Base64 and sends it as the value of the Authorization header prefixed with the string Basic. And with cURL, it’s probably easier for you to use the -u flag. But it’s also essential to know what the real request looks like. So, let’s give it a try and manually create the Authorization header.

1
2
3
4
5
6
7
8
# plain-text password
curl -u user:93a01cf0-794b-4b98-86ef-54860f36f7f3 http://localhost:8080/hello

OR

# base-64 encoded
echo -n user:93a01cf0-794b-4b98-86ef-54860f36f7f3 | base64
curl -H "Authorization: Basic dXNlcjo5M2EwMWNmMC03OTRiLTRiOTgtODZlZi01NDg2MGYzNmY3ZjM=" localhost:8080/hello

High-level overview

  • The main components acting in the authentication process for Spring Security and the relationships among these. This architecture represents the backbone of implementing authentication with Spring Security. ## Authentication process

  • Steps
    • The authentication filter delegates the authentication request to the authentication manager and, based on the response, configures the security context.
    • The authentication manager uses the authentication provider to process authentication.
    • The authentication provider implements the authentication logic.
    • The user details service implements user management responsibility, which the authentication provider uses in the authentication logic.
    • The password encoder implements password management, which the authentication provider uses in the authentication logic.
    • The security context keeps the authentication data after the authentication process.
    • An object that implements a UserDetailsService contract with Spring Security manages the details about users.
  • PasswordEncoder
    • The PasswordEncoder does two things:
      • Encodes a password
      • Verifies if the password matches an existing encoding
    • the PasswordEncoder is mandatory for the Basic authentication flow
    • a PasswordEncoder exists together with the default UserDetailsService. When we replace the default implementation of the UserDetailsService, we must also specify a PasswordEncoder
  • AuthenticationProvider
    • The AuthenticationProvider defines the authentication logic, delegating the user and password management. A default implementation of the AuthenticationProvider uses the default implementations provided for the UserDetailsService and the PasswordEncoder. Implicitly, your application secures all the endpoints.
  • Override default configurations
    • In some cases, developers choose to use beans in the Spring context for the configuration. In other cases, they override various methods for the same purpose.
    • Configuring a project with a mix of styles is not desirable as it makes the code difficult to understand and affects the maintainability of the application.

Overriding the UserDetailsService component

  • The application now uses the instance of type UserDetailsService you added to the context instead of the default autoconfigured one. But, at the same time, you won’t be able to access the endpoint anymore for two reasons:
    • You don’t have any users.
    • You don’t have a PasswordEncoder.
  • When building the instance, we have to provide the username, the password, and at least one authority. The authority is an action allowed for that user, and we can use any string for this.
  • Because we overrode UserDetailsService, we also have to declare a PasswordEncoder. Trying the example now, you’ll see an exception when you call the endpoint. When trying to do the authentication, Spring Security realizes it doesn’t know how to manage the password and fails.
  • The NoOpPasswordEncoder instance treats passwords as plain text. It doesn’t encrypt or hash them. For matching, NoOpPasswordEncoder only compares the strings using the underlying equals(Object o) method of the String class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class ProjectConfig {

  @Bean
  public UserDetailsService userDetailsService() {
    var userDetailsService = new InMemoryUserDetailsManager();

    var user = User.withUsername("john")
            .password("12345")
            .authorities("read")
            .build();

    userDetailsService.createUser(user);
    return userDetailsService;
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }
}

Overriding the endpoint authorization configuration

  • With default configuration, all the endpoints assume you have a valid user managed by the application. Also, by default, your app uses HTTP Basic authentication as the authorization method.
  • Not all endpoints of an application need to be secured, and for those that do, we might need to choose different authorization rules. To make such changes, we start by extending the WebSecurityConfigurerAdapter class. Extending this class allows us to override the configure(HttpSecurity http).
  • The permitAll() call in the configuration, together with the anyRequest() method, makes all the endpoints accessible without the need for credentials:
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  // Omitted code

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic();
    http.authorizeRequests()
           .anyRequest().permitAll();
  }
}

Overriding the AuthenticationProvider implementation

  • Below example shows the AuthenticationProvider, which implements the authentication logic and delegates to the UserDetailsService and PasswordEncoder for user and password management. So we could say that with this section, we go one step deeper in the authentication and authorization architecture to learn how to implement custom authentication logic with AuthenticationProvider
  • I recommend that you respect the responsibilities as designed in the Spring Security architecture. This architecture is loosely coupled with fine-grained responsibilities. That design is one of the things that makes Spring Security flexible and easy to integrate in your applications. But depending on how you make use of its flexibility, you could change the design as well. You have to be careful with these approaches as they can complicate your solution. For example, you could choose to override the default AuthenticationProvider in a way in which you no longer need a UserDetailsService or PasswordEncoder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

  @Override
  public Authentication authenticate (Authentication authentication) throws AuthenticationException {
    // authentication logic here
    String username = authentication.getName();
    String password = String.valueOf(authentication.getCredentials());

    if ("john".equals(username) && "12345".equals(password)) {
        return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
    } else {
        throw new AuthenticationCredentialsNotFoundException ("Error in authentication!");
    }
  }

  @Override
  public boolean supports(Class<?> authenticationType) {
    // type of the Authentication implementation here
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private CustomAuthenticationProvider authenticationProvider;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(authenticationProvider);
  }

  ...
}
  • Using multiple configuration classes in your project
    • good practice to separate the responsibilities even for the configuration classes. We need this separation because the configuration starts to become more complex.
    • You can’t have both classes extending WebSecurityConfigurerAdapter in this case. If you do so, the dependency injection fails. You might solve the dependency injection by setting the priority for injection using the @Order annotation. But, functionally, this won’t work, as the configurations exclude each other instead of merging.

Managing Users

  • Classes
    • UserDetails - which describes the user for Spring Security.
    • GrantedAuthority - which allows us to define actions that the user can execute.
    • UserDetailsManager - which extends the UserDetailsService contract. Beyond the inherited behavior, it also describes actions like creating a user and modifying or deleting a user’s password.

Implementing authentication in Spring Security

  • The UserDetailsService is only responsible for retrieving the user by username. This action is the only one needed by the framework to complete authentication.
  • The UserDetailsManager adds behavior that refers to adding, modifying, or deleting the user, which is a required functionality in most applications. The separation between the two contracts is an excellent example of the interface segregation principle. Separating the interfaces allows for better flexibility because the framework doesn’t force you to implement behavior if your app doesn’t need it. If the app only needs to authenticate the users, then implementing the UserDetailsService contract is enough to cover the desired functionality.

  • Dependencies between the components involved in user management. The UserDetailsService returns the details of a user, finding the user by its name. The UserDetails contract describes the user. A user has one or more authorities, represented by the GrantedAuthority interface. To add operations such as create, delete, or change password to the user, the UserDetailsManager contract extends UserDetailsService to add operations.

Describing the user

  • For Spring Security, a user definition should respect the UserDetails contract. The UserDetails contract represents the user as understood by Spring Security. The class of your application that describes the user has to implement this interface, and in this way, the framework understands it.
1
2
3
4
5
6
7
8
9
public interface UserDetails extends Serializable {
  String getUsername();
  String getPassword();
  Collection<? extends GrantedAuthority> getAuthorities();
  boolean isAccountNonExpired();
  boolean isAccountNonLocked();
  boolean isCredentialsNonExpired();
  boolean isEnabled();
}
  • Spring Security uses authorities to refer either to fine-grained privileges or to roles, which are groups of privileges.

Detailing on the GrantedAuthority contract

  • the actions granted for a user are called authorities.
  • The authorities represent what the user can do in your application. Without authorities, all users would be equal.
  • To create an authority, you only need to find a name for that privilege so you can refer to it later when writing the authorization rules.
1
2
3
public interface GrantedAuthority extends Serializable {
    String getAuthority();
}
  • The SimpleGrantedAuthority class offers a way to create immutable instances of the type GrantedAuthority.
    • GrantedAuthority g1 = () -> "READ";
    • GrantedAuthority g2 = new SimpleGrantedAuthority("READ");

Using a builder to create instances of the UserDetails type

The User class from the org.springframework.security.core.userdetails package is a simple way to build instances of the UserDetails type. Using this class, you can create immutable instances of UserDetails.

1
2
3
4
5
6
UserDetails u = User.withUsername("bill")
                .password("12345")
                .authorities("read", "write")
                .accountExpired(false)
                .disabled(true)
                .build();

Understanding the UserDetailsService contract

1
2
3
public interface UserDetailsService {
  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

The authentication implementation calls the loadUserByUsername(String username) method to obtain the details of a user with a given username . The username is, of course, considered unique. The user returned by this method is an implementation of the UserDetails contract. If the username doesn’t exist, the method throws a UsernameNotFoundException.

The AuthenticationProvider is the component that implements the authentication logic and uses the UserDetailsService to load details about the user. To find the user by username, it calls the loadUserByUsername(String username) method.

Implementing the UserDetailsManager contract

The Spring Security authentication flow. Here we use a JDBCUserDetailsManager as our UserDetailsService component. The JdbcUserDetailsManager uses a database to manage users.

1
2
3
4
5
6
7
public interface UserDetailsManager extends UserDetailsService {
  void createUser(UserDetails user);
  void updateUser(UserDetails user);
  void deleteUser(String username);
  void changePassword(String oldPassword, String newPassword);
  boolean userExists(String username);
}

Dealing with Passwords

Understanding the PasswordEncoder contract

1
2
3
4
5
6
7
8
9
public interface PasswordEncoder {

  String encode(CharSequence rawPassword);
  boolean matches(CharSequence rawPassword, String encodedPassword);

  default boolean upgradeEncoding(String encodedPassword) {
    return false;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Sha512PasswordEncoder implements PasswordEncoder {

  @Override
  public String encode(CharSequence rawPassword) {
    return hashWithSHA512(rawPassword.toString());
  }

  @Override
  public boolean matches(
    CharSequence rawPassword, String encodedPassword) {
    String hashedPassword = encode(rawPassword);
    return encodedPassword.equals(hashedPassword);
  }

  // Omitted code

}

Spring Security provides the following encoders out-of-the-box:

  • NoOpPasswordEncoder–Doesn’t encode the password but keeps it in cleartext. We use this implementation only for examples. Because it doesn’t hash the password, you should never use it in a real-world scenario.
  • StandardPasswordEncoder–Uses SHA-256 to hash the password. This implementation is now deprecated, and you shouldn’t use it for your new implementations. The reason why it’s deprecated is that it uses a hashing algorithm that we don’t consider strong enough anymore, but you might still find this implementation used in existing applications.
  • Pbkdf2PasswordEncoder–Uses the password-based key derivation function 2 (PBKDF2).
  • BCryptPasswordEncoder–Uses a bcrypt strong hashing function to encode the password.
  • SCryptPasswordEncoder–Uses an scrypt hashing function to encode the password.

Implementing authentication

The authentication process, which has only two possible results:

  1. The entity making the request is not authenticated. The user is not recognized, and the application rejects the request without delegating to the authorization process. Usually, in this case, the response status sent back to the client is HTTP 401 Unauthorized.
  2. The entity making the request is authenticated. The details about the requester are stored such that the application can use these for authorization. The SecurityContext interface is the instance that stores the details about the current authenticated request.

The Authentication interface represents the authentication request event and holds the details of the entity that requests access to the application. You can use the information related to the authentication request event during and after the authentication process. The user requesting access to the application is called a principal. If you’ve ever used the Java Security API in any app, you learned that in the Java Security API, an interface named Principal represents the same concept.

1
2
3
4
5
6
7
8
9
10
public interface Authentication extends Principal, Serializable {

  Collection<? extends GrantedAuthority> getAuthorities();
  Object getCredentials();
  Object getDetails();
  Object getPrincipal();
  boolean isAuthenticated();
  void setAuthenticated(boolean isAuthenticated)
     throws IllegalArgumentException;
}
1
2
3
4
5
6
public interface AuthenticationProvider {

  Authentication authenticate(Authentication authentication) throws AuthenticationException;

  boolean supports(Class<?> authentication);
}

The AuthenticationProvider responsibility is strongly coupled with the Authentication contract. The authenticate() method receives an Authentication object as a parameter and returns an Authentication object. We implement the authenticate() method to define the authentication logic.

  • The method should throw an AuthenticationException if the authentication fails.
  • If the method receives an authentication object that is not supported by your implementation of AuthenticationProvider, then the method should return null. This way, we have the possibility of using multiple Authentication types separated at the HTTP-filter level.
  • The method should return an Authentication instance representing a fully authenticated object. For this instance, the isAuthenticated() method returns true, and it contains all the necessary details about the authenticated entity. Usually, the application also removes sensitive data like a password from this instance. After implementation, the password is no longer required and keeping these details can potentially expose them to unwanted eyes.

The second method in the AuthenticationProvider interface is supports-(Class<?> authentication). You can implement this method to return true if the current AuthenticationProvider supports the type provided as an Authentication object. Observe that even if this method returns true for an object, there is still a chance that the authenticate() method rejects the request by returning null. Spring Security is designed like this to be more flexible and to allow you to implement an AuthenticationProvider that can reject an authentication request based on the request’s details, not only by its type.

AuthenticationManager delegates to one of the available authentication providers. The AuthenticationProvider might not support the provided type of authentication. On the other hand, if it does support the object type, it might not know how to authenticate that specific object. The authentication is evaluated, and an AuthenticationProvider that can say if the request is correct or not responds to the AuthenticationManager.

If the user doesn’t exist, the loadUserByUsername() method should throw an AuthenticationException. In this case, the authentication process stops, and the HTTP filter sets the response status to HTTP 401 Unauthorized. If the username exists, we can check further the user’s password with the matches() method of the PasswordEncoder from the context. If the password does not match, then again, an AuthenticationException should be thrown. If the password is correct, the AuthenticationProvider returns an instance of Authentication marked as “authenticated,” which contains the details about the request.

To plug in the new implementation of the AuthenticationProvider, override the configure(AuthenticationManagerBuilder auth) method of the WebSecurityConfigurerAdapter class in the configuration class of the project.

Using the @Autowired annotation over a field declared as an AuthenticationProvider. Spring recognizes the AuthenticationProvider as an interface (which is an abstraction). But Spring knows that it needs to find in its context an instance of an implementation for that specific interface. In our case, the implementation is the instance of CustomAuthenticationProvider, which is the only one of this type that we declared and added to the Spring context using the @Component annotation.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private AuthenticationProvider authenticationProvider;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) {
      auth.authenticationProvider(authenticationProvider);
  }

  // Omitted code
}

Using the SecurityContext

It is likely that you will need details about the authenticated entity after the authentication process ends. You might, for example, need to refer to the username or the authorities of the currently authenticated user. Is this information still accessible after the authentication process finishes?

Once the AuthenticationManager completes the authentication process successfully, it stores the Authentication instance for the rest of the request. The instance storing the Authentication object is called the security context.

1
2
3
4
public interface SecurityContext extends Serializable {
  Authentication getAuthentication();
  void setAuthentication(Authentication authentication);
}

After successful authentication, the authentication filter stores the details of the authenticated entity in the security context. From there, the controller implementing the action mapped to the request can access these details when needed.

Spring Security offers three strategies to manage the SecurityContext with an object in the role of a manager. It’s named the SecurityContextHolder:

  1. MODE_THREADLOCAL–Allows each thread to store its own details in the security context. In a thread-per-request web application, this is a common approach as each request has an individual thread.
  2. MODE_INHERITABLETHREADLOCAL–Similar to MODE_THREADLOCAL but also instructs Spring Security to copy the security context to the next thread in case of an asynchronous method. This way, we can say that the new thread running the @Async method inherits the security context.
  3. MODE_GLOBAL–Makes all the threads of the application see the same security context instance.

Using a holding strategy for the security context

The first strategy for managing the security context is the MODE_THREADLOCAL strategy. This strategy is also the default for managing the security context used by Spring Security. With this strategy, Spring Security uses ThreadLocal to manage the context. This implementation works as a collection of data but makes sure that each thread of the application can see only the data stored in the collection. This way, each request has access to its security context. No thread will have access to another’s ThreadLocal. And that means that in a web application, each request can see only its own security context. We could say that this is also what you generally want to have for a backend web application.

Below figure offers an overview of this functionality. Each request (A, B, and C) has its own allocated thread (T1, T2, and T3). This way, each request only sees the details stored in their security context. But this also means that if a new thread is created (for example, when an asynchronous method is called), the new thread will have its own security context as well. The details from the parent thread (the original thread of the request) are not copied to the security context of the new thread.

This architecture only applies to the traditional servlet application where each request has its own thread assigned. It does not apply to reactive applications.

Each request has its own thread, represented by an arrow. Each thread has access only to its own security context details. When a new thread is created (for example, by an @Async method), the details from the parent thread aren’t copied.

Being the default strategy for managing the security context, this process does not need to be explicitly configured. Just ask for the security context from the holder using the static getContext() method wherever you need it after the end of the authentication process.

Obtaining the SecurityContext from the SecurityContextHolder
1
2
3
4
5
6
7
@GetMapping("/hello")
public String hello() {
  SecurityContext context = SecurityContextHolder.getContext();
  Authentication a = context.getAuthentication();

  return "Hello, " + a.getName() + "!";
}

Obtaining the authentication from the context is even more comfortable at the endpoint level, as Spring knows to inject it directly into the method parameters. You don’t need to refer every time to the SecurityContextHolder class explicitly.

Using a holding strategy for asynchronous calls

The situation gets more complicated if we have to deal with multiple threads per request. Look at what happens if you make the endpoint asynchronous. The thread that executes the method is no longer the same thread that serves the request.

An @Async method served by a different thread
1
2
3
4
5
6
7
8
@GetMapping("/bye")
@Async
public void goodbye() {
  SecurityContext context = SecurityContextHolder.getContext();
  String username = context.getAuthentication().getName();

  // do something with the username
}
To enable the functionality of the @Async annotation
1
2
3
4
5
@Configuration
@EnableAsync
public class ProjectConfig {

}

If you try the code as it is now, it throws a NullPointerException on the line that gets the name from the authentication, which is String username = context.getAuthentication().getName().

This is because the method executes now on another thread that does not inherit the security context. For this reason, the Authorization object is null and, in the context of the presented code, causes a NullPointerException. In this case, you could solve the problem by using the MODE_INHERITABLETHREADLOCAL strategy. This can be set either by calling the SecurityContextHolder.setStrategyName() method or by using the system property spring.security.strategy. By setting this strategy, the framework knows to copy the details of the original thread of the request to the newly created thread of the asynchronous method.

Using InitializingBean to set SecurityContextHolder mode
1
2
3
4
5
6
7
8
9
10
@Configuration
@EnableAsync
public class ProjectConfig {

  @Bean
  public InitializingBean initializingBean() {
    return () -> SecurityContextHolder.setStrategyName(
      SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
  }
}

This works, however, only when the framework itself creates the thread (for example, in case of an @Async method). If your code creates the thread, you will run into the same problem even with the MODE_INHERITABLETHREADLOCAL strategy. This happens because, in this case, the framework does not know about the thread that your code creates.

Using a holding strategy for standalone applications

If what you need is a security context shared by all the threads of the application, you change the strategy to MODE_GLOBAL.

With MODE_GLOBAL used as the security context management strategy, all the threads access the same security context. This implies that these all have access to the same data and can change that information. Because of this, race conditions can occur, and you have to take care of synchronization.

Be aware that the SecurityContext is not thread safe. So, with this strategy where all the threads of the application can access the SecurityContext object, you need to take care of concurrent access.

Forwarding the security context with DelegatingSecurityContextRunnable

What happens when your code starts new threads without the framework knowing about them? Sometimes we name these self-managed threads because it is we who manage them, not the framework.

No specific strategy of the SecurityContextHolder offers you a solution to self-managed threads. In this case, you need to take care of the security context propagation. One solution for this is to use the DelegatingSecurityContextRunnable to decorate the tasks you want to execute on a separate thread. The DelegatingSecurityContextRunnable extends Runnable. You can use it following the execution of the task when there is no value expected. If you have a return value, then you can use the Callable<T> alternative, which is DelegatingSecurityContextCallable<T>. Both classes represent tasks executed asynchronously, as any other Runnable or Callable. Moreover, these make sure to copy the current security context for the thread that executes the task.

Defining an ExecutorService and submitting the task
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/ciao")
public String ciao() throws Exception {
  Callable<String> task = () -> {
      SecurityContext context = SecurityContextHolder.getContext();
      return context.getAuthentication().getName();
  };

  ExecutorService e = Executors.newCachedThreadPool();
  try {
     return "Ciao, " + e.submit(task).get() + "!";
  } finally {
     e.shutdown();
  }
}

If you run the application as is, you get nothing more than a NullPointerException. Inside the newly created thread to run the callable task, the authentication does not exist anymore, and the security context is empty. To solve this problem, we decorate the task with DelegatingSecurityContextCallable, which provides the current context to the new thread, as provided by this listing.

Running the task decorated by DelegatingSecurityContextCallable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping("/ciao")
public String ciao() throws Exception {
  Callable<String> task = () -> {
    SecurityContext context = SecurityContextHolder.getContext();
    return context.getAuthentication().getName();
  };

  ExecutorService e = Executors.newCachedThreadPool();
  try {
    var contextTask = new DelegatingSecurityContextCallable<>(task);
    return "Ciao, " + e.submit(contextTask).get() + "!";
  } finally {
    e.shutdown();
  }
}

Forwarding the security context with DelegatingSecurityContextExecutorService

  • But we have a second option to deal with the security context propagation to a new thread, and this is to manage propagation from the thread pool instead of from the task itself. An alternative to decorating tasks is to use a particular type of Executor.
  • The propagation of the security context happens because an implementation called DelegatingSecurityContextExecutorService decorates the ExecutorService. It also takes care of the security context propagation.

Propagating the SecurityContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping("/hola")
public String hola() throws Exception {
  Callable<String> task = () -> {
    SecurityContext context = SecurityContextHolder.getContext();
    return context.getAuthentication().getName();
  };

  ExecutorService e = Executors.newCachedThreadPool();
  e = new DelegatingSecurityContextExecutorService(e);
  try {
    return "Hola, " + e.submit(task).get() + "!";
  } finally {
    e.shutdown();
  }
}

Using and configuring HTTP Basic

  • For example, you might want to implement a specific logic for the case in which the authentication process fails. You might even need to set some values on the response sent back to the client in this case.
  • You can also call the httpBasic() method of the HttpSecurity instance with a parameter of type Customizer. This parameter allows you to set up some configurations related to the authentication method.
  • Also, by using a Customizer, we can customize the response for a failed authentication. You need to do this if the client of your system expects something specific in the response in the case of a failed authentication. You might need to add or remove one or more headers. Or you can have some logic that filters the body to make sure that the application doesn’t expose any sensitive data to the client.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic(c -> {
      c.realmName("OTHER");
      c.authenticationEntryPoint(new CustomEntryPoint());
    });

    http.authorizeRequests().anyRequest().authenticated();
  }
}
  • To customize the response for a failed authentication, we can implement an AuthenticationEntryPoint. Its commence() method receives the HttpServletRequest, the HttpServletResponse, and the AuthenticationException that cause the authentication to fail. Below listing demonstrates a way to implement the AuthenticationEntryPoint, which adds a header to the response and sets the HTTP status to 401 Unauthorized.
Implementing an AuthenticationEntryPoint
1
2
3
4
5
6
7
8
9
10
11
public class CustomEntryPoint implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest httpServletRequest,
    HttpServletResponse httpServletResponse, AuthenticationException e)
      throws IOException, ServletException {

      httpServletResponse.addHeader("message", "Luke, I am your father!");
      httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value());
    }
}

Configuring authorization: Restricting access

  • Authorization is the process during which the system decides if an identified client has permission to access the requested resource
  • In Spring Security, once the application ends the authentication flow, it delegates the request to an authorization filter. The filter allows or rejects the request based on the configured authorization rules.
  • When the client makes the request, the authentication filter authenticates the user. After successful authentication, the authentication filter stores the user details in the security context and forwards the request to the authorization filter. The authorization filter decides whether the call is permitted. To decide whether to authorize the request, the authorization filter uses the details from the security context.

Restricting access based on authorities and roles

  • A user has one or more authorities (actions that a user can do). During the authentication process, the UserDetailsService obtains all the details about the user, including the authorities. The application uses the authorities as represented by the GrantedAuthority interface for authorization after it successfully authenticates the user
  • An authority is an action that a user can perform with a system resource. An authority has a name that the getAuthority() behavior of the object returns as a String. We use the name of the authority when defining the custom authorization rule. Often an authorization rule can look like this: “Jane is allowed to delete the product records,” or “John is allowed to read the document records.” In these cases, delete and read are the granted authorities. The application allows the users Jane and John to perform these actions, which often have names like read, write, or delete.
The GrantedAuthority contract
1
2
3
public interface GrantedAuthority extends Serializable {
  String getAuthority();
}
The getAuthorities() method from the UserDetails contract
1
2
3
4
5
public interface UserDetails extends Serializable {
  Collection<? extends GrantedAuthority> getAuthorities();

  // Omitted code
}

Restricting access for all endpoints based on user authorities
1
2
3
4
5
6
7
8
9
var user1 = User.withUsername("john")
                .password("12345")
                .authorities("READ")
                .build();

var user2 = User.withUsername("jane")
                .password("12345")
                .authorities("WRITE")
                .build();

You provide the name of the authority allowed to the user as a parameter of the hasAuthority() method. The application needs, first, to authenticate the request and then, based on the user’s authorities, the app decides whether to allow the call.

__1. hasAuthority() method

hasAuthority('WRITE') – Stipulates that the user needs the WRITE authority to call the endpoint.

Restricting access to only users having WRITE authority
1
2
3
4
5
6
7
8
@Override
protected void configure(HttpSecurity http) throws Exception {
  http.httpBasic();

  http.authorizeRequests()
        .anyRequest()
        .hasAuthority("WRITE");
}

__2. hasAnyAuthority() method

hasAnyAuthority('READ', 'WRITE')–Specifies that the user needs one of either the READ or WRITE authorities. With this expression, you can enumerate all the authorities for which you want to allow access.

Applying the hasAnyAuthority() method
1
2
3
http.authorizeRequests()
          .anyRequest()
            .hasAnyAuthority("WRITE", "READ");

__3. access() method

To specify access based on user authorities, the third way you find in practice is the access() method. The access() method is more general, however. It receives as a parameter a Spring expression (SpEL) that specifies the authorization condition. This method is powerful, and it doesn’t refer only to authorities. However, this method also makes the code more difficult to read and understand. For this reason, I recommend it as the last option, and only if you can’t apply one of the hasAuthority() or hasAnyAuthority() methods presented earlier.

Using the access() method to configure access to the endpoints
1
2
3
http.authorizeRequests()
          .anyRequest()
            .access("hasAuthority('WRITE')");
Applying the access() method with a more complex expression
1
2
3
4
5
String expression = "hasAuthority('read') and !hasAuthority('delete')";

http.authorizeRequests()
    .anyRequest()
    .access(expression);

Restricting access for all endpoints based on user roles

  • Roles are coarse grained. Each user with a specific role can only do the actions granted by that role. When applying this philosophy in authorization, a request is allowed based on the purpose of the user in the system. Only users who have a specific role can call a certain endpoint.
  • Imagine, in your application, a user can either only have read authority or have all: read, write, and delete authorities. In this case, it might be more comfortable to think that those users who can only read have a role named READER, while the others have the role ADMIN. Having the ADMIN role means that the application grants you read, write, update, and delete privileges.
  • Roles are represented using the same contract in Spring Security GrantedAuthority. When defining a role, its name should start with the ROLE_ prefix. At the implementation level, this prefix specifies the difference between a role and an authority.
Setting roles for users
1
2
3
4
5
6
7
8
9
var user1 = User.withUsername("john")
                    .password("12345")
                    .authorities("ROLE_ADMIN")
                    .build();

    var user2 = User.withUsername("jane")
                    .password("12345")
                    .authorities("ROLE_MANAGER")
                    .build();

To set constraints for user roles, you can use one of the following methods:

  1. hasRole()–Receives as a parameter the role name for which the application authorizes the request.
  2. hasAnyRole()–Receives as parameters the role names for which the application approves the request.
  3. access()–Uses a Spring expression to specify the role or roles for which the application authorizes requests. In terms of roles, you could use hasRole() or hasAnyRole() as SpEL expressions.
Configuring the app to accept only requests from admins
1
2
http.authorizeRequests()
         .anyRequest().hasRole("ADMIN");

When building users with the User builder class as we did in the example for this section, you specify the role by using the roles() method. This method creates the GrantedAuthority object and automatically adds the ROLE_ prefix to the names you provide.

Make sure the parameter you provide for the roles() method DOES NOT include the ROLE_ prefix. If that prefix is inadvertently included in the role() parameter, the method throws an exception. In short, when using the authorities() method, include the ROLE_ prefix. When using the roles() method, do not include the ROLE_ prefix.

Important to remember that the access() method is generic. For example, to configure access to the endpoint to be allowed only after 12:00 pm, you can use the following SpEL expression:

1
T(java.time.LocalTime).now().isAfter(T(java.time.LocalTime).of(12, 0))

Restricting access to all endpoints

Using the denyAll() method to restrict access to endpoints
1
2
http.authorizeRequests()
         .anyRequest().denyAll();

Configuring authorization: Applying restrictions

Using matcher methods to select endpoints

  • We use matcher methods to choose the requests to which we apply authorization configuration.
  • Requests can be selected by path, by HTTP method or both.
  • Spring Security offers you three types of matcher methods:
  1. MVC matchers–You use MVC expressions for paths to select endpoints.
  2. Ant matchers–You use Ant expressions for paths to select endpoints.
  3. regex matchers–You use regular expressions (regex) for paths to select endpoints.

MVC matchers

  • We create an application that exposes two endpoints: /hello and /ciao. We want to make sure that only users having the ADMIN role can call the /hello endpoint. Similarly, we want to make sure that only users having the MANAGER role can call the /ciao endpoint.
  • To specify that only users having the ADMIN role can call the endpoint /hello when authorizing requests, we use the mvcMatchers() method.
1
2
3
4
5
6
7
8
@Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic();

    http.authorizeRequests()
         .mvcMatchers("/hello").hasRole("ADMIN")
         .mvcMatchers("/ciao").hasRole("MANAGER");
  }

If you now add any other endpoint to your application, it is accessible by default to anyone, even unauthenticated users.

The permitAll() method states that all other requests are allowed without authentication.

When you use matchers to refer to requests, the order of the rules should be from particular to general. This is why the anyRequest() method cannot be called before a more specific matcher method like mvcMatchers().

Marking additional requests explicitly as accessible without authentication
1
2
3
4
5
6
7
8
9
@Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic();

    http.authorizeRequests()
           .mvcMatchers("/hello").hasRole("ADMIN")
           .mvcMatchers("/ciao").hasRole("MANAGER")
           .anyRequest().permitAll();
  }

If you have designed an endpoint to be accessible to anyone, you can call it without providing a username and a password for authentication. In this case, Spring Security won’t do the authentication. If you, however, provide a username and a password, Spring Security evaluates them in the authentication process. If they are wrong (not known by the system), authentication fails, and the response status will be 401 Unauthorized. To be more precise, if you call the /hola endpoint for the configuration presented in listing 8.4, the app returns the body “Hola!” as expected, and the response status is 200 OK.

The authorization filter allows any request to the /hola path. But because the application first executes the authentication logic, the request is never forwarded to the authorization filter. Instead, the authentication filter replies with an HTTP 401 Unauthorized.

The permitAll() method refers to authorization configuration only, and if authentication fails, the call will not be allowed further.

To make all the other endpoints accessible only for authenticated users. To do this, you would change the permitAll() method with authenticated() as presented in the following listing. Similarly, you could even deny all other requests by using the denyAll() method.

Making other requests accessible for all authenticated users
1
2
3
4
5
6
7
8
9
10
@Override
  protected void configure(HttpSecurity http)
    throws Exception {
    http.httpBasic();

     http.authorizeRequests()
           .mvcMatchers("/hello").hasRole("ADMIN")
           .mvcMatchers("/ciao").hasRole("MANAGER")
           .anyRequest().authenticated();
  }

Multiple endpoints can have the same authorization rules, so you don’t have to set them up endpoint by endpoint

Selecting requests for authorization using MVC matchers

This matcher uses the standard MVC syntax for referring to paths. This syntax is the same one you use when writing endpoint mappings with annotations like @RequestMapping, @GetMapping, @PostMapping, and so forth. The two methods you can use to declare MVC matchers are as follows:

  • mvcMatchers(HttpMethod method, String... patterns)–Lets you specify both the HTTP method to which the restrictions apply and the paths. This method is useful if you want to apply different restrictions for different HTTP methods for the same path.
  • mvcMatchers(String... patterns)–Simpler and easier to use if you only need to apply authorization restrictions based on paths. The restrictions can automatically apply to any HTTP method used with the path.

Spring Security applies, by default, protection against cross-site request forgery (CSRF).

Authorization configuration for the first scenario,link
1
2
3
4
5
6
7
8
9
http.authorizeRequests()
            .mvcMatchers(HttpMethod.GET, "/a")
               .authenticated()
            .mvcMatchers(HttpMethod.POST, "/a")
               .permitAll()
            .anyRequest()
               .denyAll();

    http.csrf().disable();

For the current project, we want to ensure that the same rules apply for all requests for paths starting with /a/b. These paths in our case are /a/b and /a/b/c. To achieve this, we use the ** operator. (Spring MVC borrows the path-matching syntaxes from Ant.)

Changes in the configuration class for multiple paths
1
2
3
4
5
 http.authorizeRequests()
          .mvcMatchers( "/a/b/**")
             .authenticated()
          .anyRequest()
             .permitAll();

The ** operator refers to any number of pathnames. You can use it as we have done in the last example so that you can match requests with paths having a known prefix. You can also use it in the middle of a path to refer to any number of pathnames or to refer to paths ending in a specific pattern like /a/**/c. Therefore, /a/**/c would not only match /a/b/c but also /a/b/d/c and a/b/c/d/e/c and so on. If you only want to match one pathname, then you can use a single *. For example, a/*/c would match a/b/c and a/d/c but not a/b/d/c.

Configuring the authorization to permit only specific digits
1
2
3
4
5
http.authorizeRequests()
         .mvcMatchers("/product/{code:^[0-9]*$}") //The regex refers to strings of any length, containing any digit.
              .permitAll()
         .anyRequest()
              .denyAll();

Common expressions used for path matching with MVC matchers

Expression Description
/a Only path /a.
/a/* The * operator replaces one pathname. In this case, it matches /a/b or /a/c, but not /a/b/c.
/a/** The ** operator replaces multiple pathnames. In this case, /a as well as /a/b and /a/b/c are a match for this expression.
/a/{param} This expression applies to the path /a with a given path parameter.
/a/{param:regex} This expression applies to the path /a with a given path parameter only when the value of the parameter matches the given regular expression.

Ant matchers

The Ant matchers apply exactly the given Ant expressions for patterns but know nothing about subtle Spring MVC functionality. In this case, /hello doesn’t apply as an Ant expression to the /hello/ path. If you also want to secure the /hello/ path, you have to individually add it or write an Ant expression that matches it as well.

1
2
http.authorizeRequests()
          .antMatchers( "/hello").authenticated();

Implementing filters

  • In Spring Security, in general, HTTP filters manage each responsibility that must be applied to the request. The filters form a chain of responsibilities.
  • The filter chain receives the request. Each filter uses a manager to apply specific logic to the request and, eventually, delegates the request further along the chain to the next filter.
  • Spring Security provides filter implementations that you add to the filter chain through customization, but you can also define custom filters.

You can customize the filter chain by adding new filters before, after, or at the position of existing ones. This way, you can customize authentication as well as the entire process applied to request and response.

  • Custom filters can created by implementing the javax.servlet.Filter package.
  • Default implementations from Spring Security:
    • BasicAuthenticationFilter takes care of HTTP Basic authentication, if present.
    • CsrfFilter takes care of cross-site request forgery (CSRF) protection.
    • CorsFilter takes care of cross-origin resource sharing (CORS) authorization rules,
  • Call to the httpBasic() method, an instance of the BasicAuthenticationFilter is added to the chain.
  • Each filter has an order number. This determines the order in which filters are applied to a request. You can add custom filters along with the filters provided by Spring Security.

  • If multiple filters have the same position, the order in which they are called is not defined.

Adding a filter before an existing one in chain

  • For our example, we add a RequestValidationFilter, which acts before the authentication filter. The RequestValidationFilter ensures that authentication doesn’t happen if the validation of the request fails.
  • In our case, the request must have a mandatory header named RequestId. If the header doesn’t exist, we set an HTTP status 400 Bad Request on the response without forwarding it to the next filter in the chain.

Filter implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
    throws IOException, ServletException {

  var httpRequest = (HttpServletRequest) request;
  var httpResponse = (HttpServletResponse) response;

  String requestId = httpRequest.getHeader("Request-Id");

  if (requestId == null || requestId.isBlank()) {
      httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      return;
  }

  filterChain.doFilter(request, response);
}
Configuring the filter
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(
            new RequestValidationFilter(),
            BasicAuthenticationFilter.class)
        .authorizeRequests()
            .anyRequest().permitAll();
  }
}

Adding a filter after an existing one in chain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(
            new RequestValidationFilter(),
            BasicAuthenticationFilter.class)
        .addFilterAfter(
            new AuthenticationLoggingFilter(),
            BasicAuthenticationFilter.class)
        .authorizeRequests()
            .anyRequest().permitAll();
    }
}

Adding a filter at the location of another in the chain

  • In this section, we discuss adding a filter at the location of another one in the filter chain. You use this approach especially when providing a different implementation for a responsibility that is already assumed by one of the filters known by Spring Security.
  • A typical scenario is replacing HTTP Basic authentication flow in following scenarios:
    1. Identification based on a static header value for authentication
    2. Using a symmetric key to sign the request for authentication
    3. Using a one-time password (OTP) in the authentication process

1. Identification based on a static header value for authentication

  • This approach offers weak security related to authentication, but architects and developers often choose it in calls between backend applications for its simplicity.
  • The request contains a header with the value of the static key. If this value matches the one known by the application, it accepts the request.

2. Using a symmetric key to sign the request for authentication

  • In this scenario, using symmetric keys to sign and validate requests, both client and server know the value of a key (client and server share the key). The client uses this key to sign a part of the request (for example, to sign the value of specific headers), and the server checks if the signature is valid using the same key . The server can store individual keys for each client in a database or a secrets vault. Similarly, you can use a pair of asymmetric keys.
  • The Authorization header contains a value signed with a key known by both client and server (or a private key for which the server has the public pair). The application checks the signature and, if correct, allows the request.

3. Using a one-time password (OTP) in the authentication process

  • And finally, using an OTP in the authentication process, the user receives the OTP via a message or by using an authentication provider app like Google Authenticator.
  • To access the resource, the client has to use a one-time password (OTP). The client obtains the OTP from a third-party authentication server. Generally, applications use this approach during login when multifactor authentication is required.

Custom auth filter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class StaticKeyAuthenticationFilter implements Filter {

  @Value("${authorization.key}")
  private String authorizationKey;

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
    throws IOException, ServletException {

    var httpRequest = (HttpServletRequest) request;
    var httpResponse = (HttpServletResponse) response;

    String authentication = httpRequest.getHeader("Authorization");

    if (authorizationKey.equals(authentication)) {
        filterChain.doFilter(request, response);
    } else {
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    }
  }
}
Adding the filter in the configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private StaticKeyAuthenticationFilter filter;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.addFilterAt(filter, BasicAuthenticationFilter.class)
        .authorizeRequests()
           .anyRequest().permitAll();
  }
}
  • When UserDetailsService is not defined, Spring Boot automatically configures one. But in our scenario, we don’t need a UserDetailsService at all because the concept of the user doesn’t exist. We only validate that the user requesting to call an endpoint on the server knows a given value. Application scenarios are not usually this simple and often require a UserDetailsService. But, if you anticipate or have such a case where this component is not needed, you can disable autoconfiguration.
To disable the default UserDetailsService
1
@SpringBootApplication(exclude = {UserDetailsServiceAutoConfiguration.class })

Filter implementations from Spring Security

  • Spring Security offers a few abstract classes that implement the Filter interface and for which you can extend your filter definitions.
  • For example, you could extend the GenericFilterBean class, which allows you to use initialization parameters that you would define in a web.xml descriptor file where applicable.
  • A more useful class that extends the GenericFilterBean is OncePerRequestFilter. When adding a filter to the chain, the framework doesn’t guarantee it will be called only once per request. OncePerRequestFilter, as the name suggests, implements logic to make sure that the filter’s doFilter() method is executed only one time per request.

Applying CSRF

  • CSRF (Cross-site request forgery) protection, which is enabled by default in Spring Security
  • How CSRF works?
    • CSRF attacks assume that a user is logged into a web application. They’re tricked by the attacker into opening a page that contains scripts that execute actions in the same application the user was working on. Because the user has already logged in (as we’ve assumed from the beginning), the forgery code can now impersonate the user and do actions on their behalf.
  • How do we protect our users from such scenarios?
    • What CSRF protection wants to ensure is that only the frontend of web applications can perform mutating operations (by convention, HTTP methods other than GET, HEAD, TRACE, or OPTIONS). Then, a foreign page, like the one in our example, can’t act on behalf of the user.
  • How can we achieve this?
    • What you know for sure is that before being able to do any action that could change data, a user must send a request using HTTP GET to see the web page at least once. When this happens, the application generates a unique token.
    • The application now accepts only requests for mutating operations (POST, PUT, DELETE, and so forth) that contain this unique value in the header. The application considers that knowing the value of the token is proof that it is the app itself making the mutating request and not another system.
    • Any page containing mutating calls, like POST, PUT, DELETE, and so on, should receive through the response the CSRF token, and the page must use this token when making mutating calls.
  • Spring Security CsrfFilter
    • The starting point of CSRF protection is a filter in the filter chain called CsrfFilter.
    • The CsrfFilter intercepts requests and allows all those that use these HTTP methods: GET, HEAD, TRACE, and OPTIONS.
    • For all other requests, the filter expects to receive a header containing a token. If this header does not exist or contains an incorrect token value, the application rejects the request and sets the status of the response to HTTP 403 Forbidden.

  • What is this token, and where does it come from?
    • These tokens are nothing more than string values. You have to add the token in the header of the request when you use any method other than GET, HEAD, TRACE, or OPTIONS. If you don’t add the header containing the token, the application doesn’t accept the request.
    • The CsrfFilter uses a component named CsrfTokenRepository to manage the CSRF token values that generate new tokens, store tokens, and eventually invalidate these.
    • By default, the CsrfTokenRepository stores the token on the HTTP session and generates the tokens as random universally unique identifiers (UUIDs).

Adding the CsrfTokenLogger (shaded) after the CsrfFilter. This way, the CsrfTokenLogger can obtain the value of the token from the _csrf attribute of the request where the CsrfFilter stores it. The CsrfTokenLogger prints the CSRF token in the application console, where we can access it and use it to call an endpoint with the HTTP POST method.

While calling the POST endpoint you also need to specify the session ID (JSESSIONID) because the default implementation of CsrfTokenRepository stores the value of the CSRF token on the session

1
2
3
curl -X POST   http://localhost:8080/hello
-H 'Cookie: JSESSIONID=21ADA55E10D70BA81C338FFBB06B0206'
-H 'X-CSRF-TOKEN: 1127bfda-57b1-43f0-bce5-bacd7d94694e'
  • You use CSRF protection for web apps running in a browser, where you should expect that mutating operations can be done by the browser that loads the displayed content of the app.
  • Did you notice that the login operation in that application used HTTP POST? Then why didn’t we need to do anything explicitly about CSRF in that case? The reason why we didn’t observe this was because we didn’t develop any mutating operation within it ourselves.
  • For the default login, Spring Security correctly applies CSRF protection for us. The framework takes care of adding the CSRF token to the login request.
  • After running the application, you can access the default login page. If you inspect the form using the inspect element function of your browser, you can observe that the default implementation of the login form sends the CSRF token. This is why your login works with CSRF protection enabled even if it uses an HTTP POST request
  • The default form login uses a hidden input to send the CSRF token in the request. This is why the login request that uses an HTTP POST method works with CSRF protection enabled.

  • For any action or asynchronous JavaScript request your page uses to call a mutable action, you need to send a valid CSRF token. This is the most common way used by an application to make sure the request doesn’t come from a third party. A third-party request could try to impersonate the user to execute actions on their behalf.

Downsides of CSRF tokens

  • CSRF tokens work well in an architecture where the same server is responsible for both the frontend and the backend, mainly for its simplicity. But CSRF tokens don’t work well when the client is independent of the backend solution it consumes. This scenario happens when you have a mobile application as a client or a web frontend developed independently (Angular, ReactJS, or Vue.js). This is why you need to know how to implement the security approach for these cases as well.

Disabling CSRF for endpoints

  • By default, CSRF protection applies to any path for endpoints called with HTTP methods other than GET, HEAD, TRACE, or OPTIONS
  • CSRF can be disabled for certain endpoints only as follows:
    • Ant Expressions
      • Calling the ignoringAntMatchers(String paths) method, you can specify the Ant expressions representing the paths that you want to exclude from the CSRF protection mechanism.
    • MVC Expressions
      • A more general approach is to use a RequestMatcher. Using this allows you to apply the exclusion rules with regular MVC expressions as well as with regexes (regular expressions).
Ant expression
1
2
3
http.csrf(c -> {
        c.ignoringAntMatchers("/ciao");
    });
MVC expression
1
2
3
HandlerMappingIntrospector i = new HandlerMappingIntrospector();
MvcRequestMatcher r = new MvcRequestMatcher(i, "/ciao");
c.ignoringRequestMatchers(r);
Regex expression
1
2
3
4
String pattern = ".*[0-9].*";
String httpMethod = HttpMethod.POST.name();
RegexRequestMatcher r = new RegexRequestMatcher(pattern, httpMethod);
c.ignoringRequestMatchers(r);

Customizing CSRF

  • Another need often found in the requirements of the application is customizing the management of CSRF tokens.
  • By default, the application stores CSRF tokens in the HTTP session on the server side. This simple approach is suitable for small applications, but it’s not great for applications that serve a large number of requests and that require horizontal scaling. The HTTP session is stateful and reduces the scalability of the application.
  • Spring Security offers two contracts that you need to implement to do this:
    • CsrfToken–Describes the CSRF token itself
    • CsrfTokenRepository–Describes the object that creates, stores, and loads CSRF tokens
1
2
3
4
5
6
public interface CsrfToken extends Serializable {

  String getHeaderName();
  String getParameterName();
  String getToken();
}
  • DefaultCsrfToken implements the CsrfToken contract and creates immutable instances containing the required values: the name of the request attribute and header, and the token itself.
  • Example
    • The CsrfToken uses a custom implementation of CsrfTokenRepository. This custom implementation uses a JpaRepository to manage CSRF tokens in a database.
    • Approach 1: Tokens associated to userId: In our example, we use a table in a database to store CSRF tokens. We assume the client has an ID to identify themselves uniquely. The application needs this identifier to obtain the CSRF token and validate it. Generally, this unique ID would be obtained during login and should be different each time the user logs in. This strategy of managing tokens is similar to storing them in memory. In this case, you use a session ID. So the new identifier for this example merely replaces the session ID.
    • Approach 2: With token expiry: An alternative to this approach would be to use CSRF tokens with a defined lifetime. With such an approach, tokens expire after a time you define. You can store tokens in the database without linking them to a specific user ID. You only need to check if a token provided via an HTTP request exists and is not expired to decide whether you allow that request.

The implementation of the generateToken() method
1
2
3
4
5
@Override
public CsrfToken generateToken(HttpServletRequest httpServletRequest) {
  String uuid = UUID.randomUUID().toString();
  return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", uuid);
}

Applying CORS

  • Cross-origin resource sharing (CORS)
    • The necessity for CORS came from web applications. By default, browsers don’t allow requests made for any domain other than the one from which the site is loaded. For example, if you access the site from example.com, the browser won’t let the site make requests to api.example.com.
  • How CORS works?
    • Even if the example.org page is loaded in an iframe from the example.com domain, the calls from the content loaded in example.org won’t load. Even if the application makes a request, the browser won’t accept the response.
    • Any situation in which an application makes calls between two different domains is prohibited. (even if the port numbers are different on the same host)

  • How to resolve?
    • CORS allows you to specify from which domain your application allows requests and what details can be shared. The CORS mechanism works based on HTTP headers . The most important are
      • Access-Control-Allow-Origin–Specifies the foreign domains (origins) that can access resources on your domain.
      • Access-Control-Allow-Methods–Lets us refer only to some HTTP methods in situations in which we want to allow access to a different domain, but only to specific HTTP methods. You use this if you’re going to enable example.com to call some endpoint, but only with HTTP GET, for example.
      • Access-Control-Allow-Headers–Adds limitations to which headers you can use in a specific request.

  • To simulate the cross-origin call, we can access the page in a browser using the domain localhost. From the JavaScript code, we make the call using the IP address 127.0.0.1. Even if localhost and 127.0.0.1 refer to the same host, the browser sees these as different strings and considers these different domains
  • Pre-flight requests
    • the browser first makes a call using the HTTP OPTIONS method to test whether the request should be allowed. We call this test request a preflight request. If the preflight request fails, the browser won’t attempt to honor the original request.
    • The preflight request and the decision to make it or not are the responsibility of the browser. You don’t have to implement this logic. But it is important to understand it, so you won’t be surprised to see cross-origin calls to the backend even if you did not specify any CORS policies for specific domains.

Applying CORS policies using @CrossOrigin annotation

  • You can place the @CrossOrigin annotation directly above the method that defines the endpoint and configure it using the allowed origins and methods.
  • Allow origins: The value parameter of @CrossOrigin receives an array to let you define multiple origins; for example, @CrossOrigin({"example.com", "example.org"}).
  • You can also set the allowed headers and methods using the allowedHeaders attribute and the methods attribute of the annotation.
  • For both origins and headers, you can use the asterisk (*) to represent all headers or all origins. However, it’s always better to filter the origins and headers that you want to allow and never allow any domain to implement code that accesses your applications’ resources. By allowing all origins, you expose the application to cross-site scripting (XSS) requests, which eventually can lead to DDoS attacks
1
2
3
4
5
6
7
@PostMapping("/test")
@ResponseBody
@CrossOrigin("http://localhost:8080")
public String test() {
  logger.info("Test method called");
  return "HELLO";
}
  • Pros
    • The advantage of using @CrossOrigin to specify the rules directly where the endpoints are defined is that it creates good transparency of the rules.
  • Cons
    • The disadvantage is that it might become verbose, forcing you to repeat a lot of code. It also imposes the risk that the developer might forget to add the annotation for newly implemented endpoints.

Applying CORS using a CorsConfigurer

Defining CORS configurations centralized in the configuration class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.cors(c -> {
      CorsConfigurationSource source = request -> {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(
            List.of("example.com", "example.org"));
        config.setAllowedMethods(
            List.of("GET", "POST", "PUT", "DELETE"));
        return config;
      };
      c.configurationSource(source);
    });

    http.csrf().disable();

    http.authorizeRequests()
         .anyRequest().permitAll();
  }
}

References

  • Book - Spring Security in Action (Manning publications)