Implementing authentication logic using a custom AuthenticationProvider
Using the HTTP Basic and form-based login authentication methods
In chapters 3 and 4, we covered a few of the components acting in the authentication flow. We discussed UserDetails
and how to define the prototype to describe a user in Spring Security. We then used UserDetails
in examples where you learned how the UserDetailsService
and UserDetailsManager
contracts work and how you can implement these. We discussed and used the leading implementations of these interfaces in examples as well. Finally, you learned how a PasswordEncoder
manages the passwords and how to use one, as well as the Spring Security crypto module (SSCM) with its encryptors and key generators.
The AuthenticationProvider
layer, however, is the one responsible for the logic of authentication. The AuthenticationProvider
is where you find the conditions and instructions that decide whether to authenticate a request or not. The component that delegates this responsibility to the AuthenticationProvider
is the AuthenticationManager
, which receives the request from the HTTP filter layer. We’ll discuss the filters layer in detail in chapter 9. In this chapter, let’s look at the authentication process, which has only two possible results:
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.
The entity making the request is authenticated. The details about the requester are stored such that the application can use these for authorization. As you’ll find out in this chapter, the SecurityContext
interface is the instance that stores the details about the current authenticated request.
To remind you of the actors and the links between them, figure 5.1 provides the diagram that you also saw in chapter 2.
This chapter covers the remaining parts of the authentication flow (the shaded boxes in figure 5.1). Then, in chapters 7 and 8, you’ll learn how authorization works, which is the process that follows authentication in the HTTP request. First, we need to discuss how to implement the AuthenticationProvider
interface. You need to know how Spring Security understands a request in the authentication process.
In enterprise applications, you might find yourself in a situation in which the default implementation of authentication based on username and password does not apply. Additionally, when it comes to authentication, your application may require the implementation of several scenarios (figure 5.2). For example, you might want the user to be able to prove who they are by using a code received in an SMS message or displayed by a specific application. Or, you might need to implement authentication scenarios where the user has to provide a certain kind of key stored in a file. You might even need to use a representation of the user’s fingerprint to implement the authentication logic. A framework’s purpose is to be flexible enough to allow you to implement any of these required scenarios.
A framework usually provides a set of the most commonly used implementations, but it cannot, of course, cover all the possible options. In terms of Spring Security, you can use the AuthenticationProvider
contract to define any custom authentication logic. In this section, you learn to represent the authentication event by implementing the Authentication
interface and then creating your custom authentication logic with an AuthenticationProvider
. To achieve our goal
In section 5.1.1, we analyze how Spring Security represents the authentication event.
In section 5.1.2, we discuss the AuthenticationProvider
contract, which is responsible for the authentication logic.
In section 5.1.3, you write custom authentication logic by implementing the AuthenticationProvider
contract in an example.
In this section, we discuss how Spring Security represents a request during the authentication process. It is important to touch on this before diving into implementing custom authentication logic. As you’ll learn in section 5.1.2, to implement a custom AuthenticationProvider,
you first need to understand how to represent the authentication event itself. In this section, we take a look at the contract representing authentication and discuss the methods you need to know.
Authentication
is one of the essential interfaces involved in the process with the same name. 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.
The Authentication
interface of Spring Security extends this contract (figure 5.3).
The Authentication
contract in Spring Security not only represents a principal, it also adds information on whether the authentication process finishes, as well as a collection of authorities. The fact that this contract was designed to extend the Principal
contract from the Java Security API is a plus in terms of compatibility with implementations of other frameworks and applications. This flexibility allows for more facile migrations to Spring Security from applications that implement authentication in another fashion.
Let’s find out more about the design of the Authentication
interface, in the following listing.
public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
For the moment, the only methods of this contract that you need to learn are these:
isAuthenticated()
--Returns true if the authentication process ends or false if the authentication process is still in progress.
getCredentials()
--Returns a password or any secret used in the process of authentication.
getAuthorities()
--Returns a collection of granted authorities for the authenticated request.
We’ll discuss the other methods for the Authentication
contract in later chapters, where appropriate to the implementations we look at then.
In this section, we discuss implementing custom authentication logic. We analyze the Spring Security contract related to this responsibility to understand its definition. With these details, you implement custom authentication logic with a code example in section 5.1.3.
The AuthenticationProvider
in Spring Security takes care of the authentication logic. The default implementation of the AuthenticationProvider
interface delegates the responsibility of finding the system’s user to a UserDetailsService
. It uses the PasswordEncoder
as well for password management in the process of authentication. The following listing gives the definition of the AuthenticationProvider
, which you need to implement to define a custom authentication provider for your application.
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. We can quickly summarize the way you should implement the authenticate()
method with three bullets:
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. We’ll discuss this aspect more in chapter 9. You’ll also find an example having multiple AuthorizationProvider
classes in chapter 11, which is the second hands-on chapter of this book.
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.
An analogy of how the authentication manager and authentication provider work together to validate or invalidate an authentication request is having a more complex lock for your door. You can open this lock either by using a card or an old fashioned physical key (figure 5.4). The lock itself is the authentication manager that decides whether to open the door. To make that decision, it delegates to the two authentication providers: one that knows how to validate the card or the other that knows how to verify the physical key. If you present a card to open the door, the authentication provider that works only with physical keys complains that it doesn’t know this kind of authentication. But the other provider supports this kind of authentication and verifies whether the card is valid for the door. This is actually the purpose of the supports()
methods.
Besides testing the authentication type, Spring Security adds one more layer for flexibility. The door’s lock can recognize multiple kinds of cards. In this case, when you present a card, one of the authentication providers could say, “I understand this as being a card. But it isn’t the type of card I can validate!” This happens when supports()
returns true but authenticate()
returns null.
In this section, we implement custom authentication logic. You can find this example in the project ssia-ch5-ex1. With this example, you apply what you’ve learned about the Authentication
and AuthenticationProvider
interfaces in sections 5.1.1 and 5.1.2. In listings 5.3 and 5.4, we build, step by step, an example of how to implement a custom AuthenticationProvider
. These steps, also presented in figure 5.5, follow:
Declare a class that implements the AuthenticationProvider
contract.
Decide which kinds of Authentication
objects the new Authentication-Provider
supports:
Register an instance of the new AuthenticationProvider
implementation with Spring Security.
@Component public class CustomAuthenticationProvider implements AuthenticationProvider { // Omitted code @Override public boolean supports(Class<?> authenticationType) { return authenticationType .equals(UsernamePasswordAuthenticationToken.class); } }
In listing 5.3, we define a new class that implements the AuthenticationProvider
interface. We mark the class with @Component
to have an instance of its type in the context managed by Spring. Then, we have to decide what kind of Authentication
interface implementation this AuthenticationProvider
supports. That depends on what type we expect to be provided as a parameter to the authenticate()
method. If we don’t customize anything at the authentication-filter level (which is our case, but we’ll do that when reaching chapter 9), then the class UsernamePasswordAuthenticationToken
defines the type. This class is an implementation of the Authentication
interface and represents a standard authentication request with username and password.
With this definition, we made the AuthenticationProvider
support a specific kind of key. Once we have specified the scope of our AuthenticationProvider,
we implement the authentication logic by overriding the authenticate()
method as shown in following listing.
@Component public class CustomAuthenticationProvider implements AuthenticationProvider { @Autowired private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Override public Authentication authenticate(Authentication authentication) { String username = authentication.getName(); String password = authentication.getCredentials().toString(); UserDetails u = userDetailsService.loadUserByUsername(username); if (passwordEncoder.matches(password, u.getPassword())) { return new UsernamePasswordAuthenticationToken( username, password, u.getAuthorities()); ❶ } else { throw new BadCredentialsException ("Something went wrong!"); ❷ } } // Omitted code }
❶ If the password matches, returns an implementation of the Authentication contract with the necessary details
❷ If the password doesn’t match, throws an exception of type AuthenticationException. BadCredentialsException inherits from AuthenticationException.
The logic in listing 5.4 is simple, and figure 5.5 shows this logic visually. We make use of the UserDetailsService
implementation to get the UserDetails
. 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. This is demonstrated in the following listing.
@Configuration public class ProjectConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationProvider authenticationProvider; @Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(authenticationProvider); } // Omitted code }
NOTE In listing 5.5, I use 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.
That’s it! You successfully customized the implementation of the AuthenticationProvider
. You can now customize the authentication logic for your application where you need it.
This section discusses the security context. We analyze how it works, how to access data from it, and how the application manages it in different thread-related scenarios. Once you finish this section, you’ll know how to configure the security context for various situations. This way, you can use the details about the authenticated user stored by the security context in configuring authorization in chapters 7 and 8.
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.
The security context of Spring Security is described by the SecurityContext
interface. The following listing defines this interface.
public interface SecurityContext extends Serializable { Authentication getAuthentication(); void setAuthentication(Authentication authentication); }
As you can observe from the contract definition, the primary responsibility of the SecurityContext
is to store the Authentication
object. But how is the SecurityContext
itself managed? Spring Security offers three strategies to manage the SecurityContext
with an object in the role of a manager. It’s named the SecurityContextHolder
:
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.
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.
MODE_GLOBAL
--Makes all the threads of the application see the same security context instance.
Besides these three strategies for managing the security context provided by Spring Security, in this section, we also discuss what happens when you define your own threads that are not known by Spring. As you will learn, for these cases, you need to explicitly copy the details from the security context to the new thread. Spring Security cannot automatically manage objects that are not in Spring’s context, but it offers some great utility classes for this.
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. ThreadLocal
is an implementation provided by the JDK. 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.
Figure 5.7 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.
NOTE Here we discuss a traditional servlet application where each request is tied to a 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. We’ll discuss the security for reactive approaches in detail in chapter 19.
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. In listing 5.7, you find an example of obtaining the security context in one of the endpoints of the application. From the security context, you can further get the Authentication
object, which stores the details about the authenticated entity. You can find the examples we discuss in this section as part of the project ssia-ch5-ex2.
@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. This approach, as presented in the following listing, is better.
@GetMapping("/hello")
public String hello(Authentication a) { ❶
return "Hello, " + a.getName() + "!";
}
❶ Spring Boot injects the current Authentication in the method parameter.
When calling the endpoint with a correct user, the response body contains the username. For example,
curl -u user:99ff79e3-8ca0-401c-a396-0a8625ab3bad http://localhost:8080/hello Hello, user!
It is easy to stick with the default strategy for managing the security context. And in a lot of cases, it is the only thing you need. MODE_THREADLOCAL
offers you the ability to isolate the security context for each thread, and it makes the security context more natural to understand and manage. But there are also cases in which this does not apply.
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. Think about an endpoint like the one presented in the next listing.
@GetMapping("/bye")
@Async ❶
public void goodbye() {
SecurityContext context = SecurityContextHolder.getContext();
String username = context.getAuthentication().getName();
// do something with the username
}
❶ Being @Async, the method is executed on a separate thread.
To enable the functionality of the @Async
annotation, I have also created a configuration class and annotated it with @EnableAsync
, as shown here:
@Configuration @EnableAsync public class ProjectConfig { }
NOTE Sometimes in articles or forums, you find that the configuration annotations are placed over the main class. For example, you might find that certain examples use the @EnableAsync
annotation directly over the main class. This approach is technically correct because we annotate the main class of a Spring Boot application with the @SpringBootApplication
annotation, which includes the @Configuration
characteristic. But in a real-world application, we prefer to keep the responsibilities apart, and we never use the main class as a configuration class. To make things as clear as possible for the examples in this book, I prefer to keep these annotations over the @Configuration
class, similar to how you’ll find them in practical scenarios.
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 (figure 5.8).
The next listing presents a way to set the security context management strategy by calling the setStrategyName()
method.
@Configuration @EnableAsync public class ProjectConfig { @Bean public InitializingBean initializingBean() { return () -> SecurityContextHolder.setStrategyName( SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); } }
Calling the endpoint, you will observe now that the security context is propagated correctly to the next thread by Spring. Additionally, Authentication
is not null anymore.
NOTE 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. We’ll discuss how to solve the issues of these cases in sections 5.2.4 and 5.2.5.
If what you need is a security context shared by all the threads of the application, you change the strategy to MODE_GLOBAL
(figure 5.9). You would not use this strategy for a web server as it doesn’t fit the general picture of the application. A backend web application independently manages the requests it receives, so it really makes more sense to have the security context separated per request instead of one context for all of them. But this can be a good use for a standalone application.
As the following code snippet shows, you can change the strategy in the same way we did with MODE_INHERITABLETHREADLOCAL
. You can use the method SecurityContextHolder.setStrategyName()
or the system property spring.security .strategy
:
@Bean public InitializingBean initializingBean() { return () -> SecurityContextHolder.setStrategyName( SecurityContextHolder.MODE_GLOBAL); }
Also, 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.
You have learned that you can manage the security context with three modes provided by Spring Security: MODE_THREADLOCAL
, MODE_INHERITEDTHREADLOCAL
, and MODE_GLOBAL
. By default, the framework only makes sure to provide a security context for the thread of the request, and this security context is only accessible to that thread. But the framework doesn’t take care of newly created threads (for example, in case of an asynchronous method). And you learned that for this situation, you have to explicitly set a different mode for the management of the security context. But we still have a singularity: 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. In this section, we apply some utility tools provided by Spring Security that help you propagate the security context to newly created threads.
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. As figure 5.10 shows, these objects decorate the original tasks and copy the security context to the new threads.
Listing 5.11 presents the use of DelegatingSecurityContextCallable
. Let’s start by defining a simple endpoint method that declares a Callable
object. The Callable
task returns the username from the current security context.
@GetMapping("/ciao") public String ciao() throws Exception { Callable<String> task = () -> { SecurityContext context = SecurityContextHolder.getContext(); return context.getAuthentication().getName(); }; ... }
We continue the example by submitting the task to an ExecutorService
. The response of the execution is retrieved and returned as a response body by the endpoint.
@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.
@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(); } }
Calling the endpoint now, you can observe that Spring propagated the security context to the thread in which the tasks execute:
curl -u user:2eb3f2e8-debd-420c-9680-48159b2ff905
➥ http://localhost:8080/ciao
The response body for this call is
Ciao, user!
When dealing with threads that our code starts without letting the framework know about them, we have to manage propagation of the details from the security context to the next thread. In section 5.2.4, you applied a technique to copy the details from the security context by making use of the task itself. Spring Security provides some great utility classes like DelegatingSecurityContextRunnable
and DelegatingSecurityContextCallable
. These classes decorate the tasks you execute asynchronously and also take the responsibility to copy the details from security context such that your implementation can access those from the newly created thread. 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. In this section, you learn how to apply this technique by using more great utility classes provided by Spring Security.
An alternative to decorating tasks is to use a particular type of Executor
. In the next example, you can observe that the task remains a simple Callable<T>
, but the thread still manages the security context. The propagation of the security context happens because an implementation called DelegatingSecurityContextExecutorService
decorates the ExecutorService
. The DelegatingSecurityContext-ExecutorService
also takes care of the security context propagation, as presented in figure 5.11.
The code in listing 5.14 shows how to use a DelegatingSecurityContext- ExecutorService
to decorate an ExecutorService
such that when you submit the task, it takes care to propagate the details of the security context.
@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(); } }
Call the endpoint to test that the DelegatingSecurityContextExecutorService
correctly delegated the security context:
curl -u user:5a5124cc-060d-40b1-8aad-753d3da28dca http://localhost:8080/hola
The response body for this call is
Hola, user!
Note Of the classes that are related to concurrency support for the security context, I recommend you be aware of the ones presented in table 5.1.
Spring offers various implementations of the utility classes that you can use in your application to manage the security context when creating your own threads. In section 5.2.4, you implemented DelegatingSecurityContextCallable
. In this section, we use DelegatingSecurityContextExecutorService
. If you need to implement security context propagation for a scheduled task, then you will be happy to hear that Spring Security also offers you a decorator named DelegatingSecurityContextScheduledExecutorService
. This mechanism is similar to the DelegatingSecurityContextExecutorService
that we presented in this section, with the difference that it decorates a ScheduledExecutorService
, allowing you to work with scheduled tasks.
Additionally, for more flexibility, Spring Security offers you a more abstract version of a decorator called DelegatingSecurityContextExecutor
. This class directly decorates an Executor
, which is the most abstract contract of this hierarchy of thread pools. You can choose it for the design of your application when you want to be able to replace the implementation of the thread pool with any of the choices the language provides you.
Up to now, we’ve only used HTTP Basic as the authentication method, but throughout this book, you’ll learn that there are other possibilities as well. The HTTP Basic authentication method is simple, which makes it an excellent choice for examples and demonstration purposes or proof of concept. But for the same reason, it might not fit all of the real-world scenarios that you’ll need to implement.
In this section, you learn more configurations related to HTTP Basic. As well, we discover a new authentication method called the formLogin
. For the rest of this book, we’ll discuss other methods for authentication, which match well with different kinds of architectures. We’ll compare these such that you understand the best practices as well as the anti-patterns for authentication.
You are aware that HTTP Basic is the default authentication method, and we have observed the way it works in various examples in chapter 3. In this section, we add more details regarding the configuration of this authentication method.
For theoretical scenarios, the defaults that HTTP Basic authentication comes with are great. But in a more complex application, you might find the need to customize some of these settings. 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. So let’s consider these cases with practical examples to understand how you can implement this. I want to point out again how you can set this method explicitly, as shown in the following listing. You can find this example in the project ssia-ch5-ex3.
@Configuration public class ProjectConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic(); } }
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, for example, the realm name, as shown in listing 5.16. You can think about the realm as a protection space that uses a specific authentication method. For a complete description, refer to RFC 2617 at https://tools.ietf.org/html/rfc2617.
@Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic(c -> { c.realmName("OTHER"); }); http.authorizeRequests().anyRequest().authenticated(); }
Listing 5.16 presents an example of changing the realm name. The lambda expression used is, in fact, an object of type Customizer<HttpBasicConfigurer- <HttpSecurity>>
. The parameter of type HttpBasicConfigurer<HttpSecurity>
allows us to call the realmName()
method to rename the realm. You can use cURL with the -v
flag to get a verbose HTTP response in which the realm name is indeed changed. However, note that you’ll find the WWW-Authenticate
header in the response only when the HTTP response status is 401 Unauthorized and not when the HTTP response status is 200 OK. Here’s the call to cURL:
curl -v http://localhost:8080/hello
/ ... < WWW-Authenticate: Basic realm="OTHER" ...
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.
NOTE Always exercise caution about the data that you expose outside of the system. One of the most common mistakes (which is also part of the OWASP top ten vulnerabilities) is exposing sensitive data. Working with the details that the application sends to the client for a failed authentication is always a point of risk for revealing confidential information.
To customize the response for a failed authentication, we can implement an AuthenticationEntryPoint
. Its commence()
method receives the HttpServlet-Request
, the HttpServletResponse
, and the AuthenticationException
that cause the authentication to fail. Listing 5.17 demonstrates a way to implement the AuthenticationEntryPoint,
which adds a header to the response and sets the HTTP status to 401 Unauthorized.
NOTE It’s a little bit ambiguous that the name of the AuthenticationEntryPoint
interface doesn’t reflect its usage on authentication failure. In the Spring Security architecture, this is used directly by a component called ExceptionTranslationManager
, which handles any AccessDeniedException
and AuthenticationException
thrown within the filter chain. You can view the ExceptionTranslationManager
as a bridge between Java exceptions and HTTP responses.
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()); } }
You can then register the CustomEntryPoint
with the HTTP Basic method in the configuration class. The following listing presents the configuration class for the custom entry point.
@Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic(c -> { c.realmName("OTHER"); c.authenticationEntryPoint(new CustomEntryPoint()); }); http.authorizeRequests() .anyRequest() .authenticated(); }
If you now make a call to an endpoint such that the authentication fails, you should find in the response the newly added header:
curl -v http://localhost:8080/hello
... < HTTP/1.1 401 < Set-Cookie: JSESSIONID=459BAFA7E0E6246A463AD19B07569C7B; Path=/; HttpOnly < message: Luke, I am your father! ...
When developing a web application, you would probably like to present a user-friendly login form where the users can input their credentials. As well, you might like your authenticated users to be able to surf through the web pages after they logged in and to be able to log out. For a small web application, you can take advantage of the form-based login method. In this section, you learn to apply and configure this authentication method for your application. To achieve this, we write a small web application that uses form-based login. Figure 5.12 describes the flow we’ll implement. The examples in this section are part of the project ssia-ch5-ex4.
NOTE I link this method to a small web application because, this way, we use a server-side session for managing the security context. For larger applications that require horizontal scalability, using a server-side session for managing the security context is undesirable. We’ll discuss these aspects in more detail in chapters 12 through 15 when dealing with OAuth 2.
To change the authentication method to form-based login, in the configure (HttpSecurity http)
method of the configuration class, instead of httpBasic()
, call the formLogin()
method of the HttpSecurity
parameter. The following listing presents this change.
@Configuration public class ProjectConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin(); http.authorizeRequests().anyRequest().authenticated(); } }
Even with this minimal configuration, Spring Security has already configured a login form, as well as a log-out page for your project. Starting the application and accessing it with the browser should redirect you to a login page (figure 5.13).
You can log in using the default provided credentials as long as you do not register your UserDetailsService
. These are, as we learned in chapter 2, username “user” and a UUID password that is printed in the console when the application starts. After a successful login, because there is no other page defined, you are redirected to a default error page. The application relies on the same architecture for authentication that we encountered in previous examples. So, like figure 5.14 shows, you need to implement a controller for the homepage of the application. The difference is that instead of having a simple JSON-formatted response, we want the endpoint to return HTML that can be interpreted by the browser as our web page. Because of this, we choose to stick to the Spring MVC flow and have the view rendered from a file after the execution of the action defined in the controller. Figure 5.14 presents the Spring MVC flow for rendering the homepage of the application.
To add a simple page to the application, you first have to create an HTML file in the resources/static folder of the project. I call this file home.html. Inside it, type some text that you will be able to find afterward in the browser. You can just add a heading (for example, <h1>Welcome</h1>
). After creating the HTML page, a controller needs to define the mapping from the path to the view. The following listing presents the definition of the action method for the home.html page in the controller class.
@Controller public class HelloController { @GetMapping("/home") public String home() { return "home.html"; } }
Mind that it is not a @RestController
but a simple @Controller
. Because of this, Spring does not send the value returned by the method in the HTTP response. Instead, it finds and renders the view with the name home.html.
Trying to access the /home path now, you are first asked if you want to log in. After a successful login, you are redirected to the homepage, where the welcome message appears. You can now access the /logout path, and this should redirect you to a log-out page (figure 5.15).
After attempting to access a path without being logged in, the user is automatically redirected to the login page. After a successful login, the application redirects the user back to the path they tried to originally access. If that path does not exist, the application displays a default error page. The formLogin()
method returns an object of type FormLoginConfigurer<HttpSecurity>
, which allows us to work on customizations. For example, you can do this by calling the defaultSuccessUrl()
method, as shown in the following listing.
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .defaultSuccessUrl("/home", true); http.authorizeRequests() .anyRequest().authenticated(); }
If you need to go even more in depth with this, using the AuthenticationSuccessHandler
and AuthenticationFailureHandler
objects offers a more detailed customization approach. These interfaces let you implement an object through which you can apply the logic executed for authentication. If you want to customize the logic for successful authentication, you can define an AuthenticationSuccessHandler
. The onAuthenticationSuccess()
method receives the servlet request, servlet response, and the Authentication
object as parameters. In listing 5.22, you’ll find an example of implementing the onAuthenticationSuccess()
method to make different redirects depending on the granted authorities of the logged-in user.
@Component public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess( HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException { var authorities = authentication.getAuthorities(); var auth = authorities.stream() .filter(a -> a.getAuthority().equals("read")) .findFirst(); ❶ if (auth.isPresent()) { ❷ httpServletResponse .sendRedirect("/home"); } else { httpServletResponse .sendRedirect("/error"); } } }
❶ Returns an empty Optional object if the “read” authority doesn’t exist
❷ If the “read” authority exists, redirects to /home
There are situations in practical scenarios when a client expects a certain format of the response in case of failed authentication. They may expect a different HTTP status code than 401 Unauthorized or additional information in the body of the response. The most typical case I have found in applications is to send a request identifier. This request identifier has a unique value used to trace back the request among multiple systems, and the application can send it in the body of the response in case of failed authentication. Another situation is when you want to sanitize the response to make sure that the application doesn’t expose sensitive data outside of the system. You might want to define custom logic for failed authentication simply by logging the event for further investigation.
If you would like to customize the logic that the application executes when authentication fails, you can do this similarly with an AuthenticationFailureHandler
implementation. For example, if you want to add a specific header for any failed authentication, you could do something like that shown in listing 5.23. You could, of course, implement any logic here as well. For the AuthenticationFailureHandler
, onAuthenticationFailure()
receives the request, response, and the Authentication
object.
@Component public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure( HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) { httpServletResponse .setHeader("failed", LocalDateTime.now().toString()); } }
To use the two objects, you need to register them in the configure()
method on the FormLoginConfigurer
object returned by the formLogin()
method. The following listing shows how to do this.
@Configuration public class ProjectConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private CustomAuthenticationFailureHandler authenticationFailureHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler); http.authorizeRequests() .anyRequest().authenticated(); } }
For now, if you try to access the /home path using HTTP Basic with the proper username and password, you are returned a response with the status HTTP 302 Found. This response status code is how the application tells you that it is trying to do a redirect. Even if you have provided the right username and password, it won’t consider these and will instead try to send you to the login form as requested by the formLogin
method. You can, however, change the configuration to support both the HTTP Basic and the form-based login methods, as in the following listing.
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler) .and() .httpBasic(); http.authorizeRequests() .anyRequest().authenticated(); }
Accessing the /home path now works with both the form-based login and HTTP Basic authentication methods:
curl -u user:cdd430f6-8ebc-49a6-9769-b0f3ce571d19
➥ http://localhost:8080/home
<h1>Welcome</h1>
The AuthenticationProvider
is the component that allows you to implement custom authentication logic.
When you implement custom authentication logic, it’s a good practice to keep the responsibilities decoupled. For user management, the Authentication Provider
delegates to a UserDetailsService
, and for the responsibility of password validation, the AuthenticationProvider
delegates to a PasswordEncoder
.
The SecurityContext
keeps details about the authenticated entity after successful authentication.
You can use three strategies to manage the security context: MODE _THREADLOCAL
, MODE_INHERITABLETHREADLOCAL
, and MODE_GLOBAL
. Access from different threads to the security context details works differently depending on the mode you choose.
Remember that when using the shared-thread local mode, it’s only applied for threads that are managed by Spring. The framework won’t copy the security context for the threads that are not governed by it.
Spring Security offers you great utility classes to manage the threads created by your code, about which the framework is now aware. To manage the SecurityContext
for the threads that you create, you can use
Spring Security autoconfigures a form for login and an option to log out with the form-based login authentication method, formLogin()
. It is straightforward to use when developing small web applications.
The formLogin
authentication method is highly customizable. Moreover, you can use this type of authentication together with the HTTP Basic method.
18.222.155.58