Spring Security Authentication Process : Authentication Flow Behind the Scenes
In every application, Authentication is the first layer of protection. Thus, Authentication is a major part of Application Security and in this post we will dive deep into the architecture used by Spring Security for Authentication.
What is Authentication? Authentication is proving who you are. For example, your Identity Card is used to “authenticate” you in your School/College/Office campus.
Spring Security provides excellent support for Authentication by default. Kind of in a plug-in-and-play fashion. A simple Authentication workflow is really easy to setup. However, if you wanna go for a complete custom implementation then you need to understand the authentication flow and comprehend what happens behind the scenes when an authentication request is received by the server.
Behind the Scenes: Theory
Components:
- Authentication Filter : Its a Filter in the FilterChain which detects an authentication attempt and forwards it to the AuthenticationManager.
2. Authentication : This component specifies the type of authentication to be conducted. Its is an interface. Its implementation specifies the type of Authentication
. For example, UsernamePasswordAuthenticationToken
is an implementation of the Authentication
interface which specifies that the user wants to authenticate using a username and password. Other examples include OpenIDAuthenticationToken
, RememberMeAuthenticationToken
.
3. Authentication Manager : The main job of this component is to delegate the authenticate() call to the correct AuthenticationProvider
. An application can have multiple AuthenticationProvider
s, few of which are DaoAuthenticationProvider
, LdapAuthenticationProvider
, OpenIDAuthenticationProvider
, etc. ). The Authentication Manager decides which Authentication Provider to delegate the call to by calling the supports() method on every available AuthenticationProvider. If the supports() method returns true then that AuthenticationProvider
supports the Authentication
type and is used to perform authentication.
In this diagram, we can see there are three Authentication Providers. Out of the three, Authentication Provider 2 supports the type of incoming Authentication as its supports() method returns true. It then performs authentication and on success, returns an Authentication object of the same type with the authenticated property set to true along with some other relevant properties.
4. Authentication Provider : It is an interface whose implementation processes a certain type of authentication . An AuthenticaionProvider
has an authenticate method which takes in the Authentication
type and performs authentication on it. On successful authentication, the AuthenticationProvider
returns back an Authentication
object of the same type that was taken as input with the authenticated property set to true. If authentication fails, then it throws an Authentication Exception. Following figure shows a generalized AuthenticationProvider :
In most apps, we perform username and password authentication and therefore, before performing authentication we have to fetch the UserDetails (username / email, password, roles, etc..) from a “datasource” such as a database with the help of an UserService, and then authenticate the provided data against the actual data. The following figure demonstrates the process of a DaoAuthenticationProvider ( this is an implementation of the AuthenticationProvider interface which mainly deals with username password authentication ):
5. UserDetailsService: This is a service which is responsible for fetching the details of the user from a “datasource”, most likely a database using the loadUserByUsername(String username) methods which takes in the username as a parameter. It then returns a UserDetails object populated with the user data fetched from the datasource ( database ). The three main fields of an UserDetails object are username, password and the roles/authorities.
The Big Picture
Time to put everything together. We are going to look at the entire flow of a username and password authentication with the help of a diagram. This is a fairly large diagram so I am going to split it into two parts and we are going to break the entire process into detailed steps based on the components we just learnt about—
Step 1 : When the server receives a request for authentication, such as a login request, it is first intercepted by the Authentication Filter in the Filter Chain.
Step 2 : A UsernamePasswordAuthenticationToken is created using the username and password provided by the user. As discussed above, the UsernamePasswordAuthenticationToken is an implementation of the Authentication interface and used when a user wants to authenticate using a username and password. Kind of self explanatory, isn’t it ?
Step 3 : The UsernamePasswordAuthenticationToken is passed to the AuthenticationManager so that the token can be authenticated.
Step 4 : The AuthenticationManager delegates the authentication to the appropriate AuthenticationProvider. As dicussed before this is accomplished by calling the supports() method on the AuthenticationProvider.
Step 5 : The AuthenticationProvider calls the loadUserByUsername(username) method of the UserDetailsService and gets back the UserDetails object containing all the data of the user. The most important data is the password becuase it will be used to check whether the provided password is correct. If no user is found with the given user name, a UsernameNotFoundException is thrown.
Step 6 : The AuthenticationProvider after receiving the UserDetails checks the passwords and authenticates the user. FINALLY!!! . If the passwords do not match it throws a AuthenticationException. However, if the authentication is successful, a UsernamePasswordAuthenticationToken is created, and the fields principal, credentials, and authenticated are set to appropriate values . Here principal refers to your username or the UserDetails , credentials refers to password and the authenticated field is set to true. This token is returned back to the AuthenticationManager.
Step 7: On successful authentication, the SecurityContext is updated with the details of the current authenticated user. SecurityContext can be used in several parts of the app to check whether any user is currently authenticated and if so, what are the user’s details.
SO THATS THE ENTIRE PROCESS !!!!!
So we had a very detailed look at the theoretical aspects. Now let’s do some hands-on exercise and see what we have learnt in action.
Behind the Scenes : Practical
Objective
We are going to build a REST API using Spring Boot. We are going to expose an an endpoint “/auth/login” which takes the username and password as the request body. On successful authentication, the API will return a JSON object with one field — authenticated , set to true.
Setup
- Go to start.sring.io and create a Spring Boot project. Add the following dependencies :
- Spring Web
- Spring Security
Your page should like something like this .
2. Generate Project and extract the downloaded ZIP folder.
3. Open Eclipse ( or your favorite IDE ) and Import the project as a Maven Project.
Coding
- pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.1</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><name>demo</name><description>Demo project for Spring Boot</description><properties><java.version>11</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
2. Create the following packages — models, rest, security and service.
3. In the models package create two classes AuthReqBody.java and AuthResBody.java.
- AuthReqBody.java — Model for Request Body
package com.example.demo.models;public class AuthReqBody {private String username;private String password;public AuthReqBody(String username, String password) {this.username = username;this.password = password;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}@Overridepublic String toString() {return "AuthReqBody [username=" + username + ", password=" + password + "]";}}
- AuthResBody.java — Model for Request Response
package com.example.demo.models;public class AuthResBody {private final Boolean authenticated;public AuthResBody(Boolean authenticated) {this.authenticated = authenticated;}public Boolean getAuthenticated() {return authenticated;}}
4. In the security package create two classes WebSecurityConfig.java and MyAuthProvider.java
- WebSecurityConfig.java — The Security Configuration Class which disables csrf protection, enable cors and open all routes.
package com.example.demo.security;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.cors()
.and()
.authorizeRequests().antMatchers("/**").permitAll();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}}
- MyAuthProvider.java — This is our custom AuthProvider class. In this class we override the authenticate method. We fetch the UserDetails from the UserDetailsService by the username and compare the passwords. If the passwords match we return a UsernamePasswordAuthenticationToken in which authenticated is set to true. If not we throw a RuntimeException.
package com.example.demo.security;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import com.example.demo.service.MyUserDetailsService;@Component
public class MyAuthProvider implements AuthenticationProvider {@Autowired
private MyUserDetailsService userDetailsService;@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
System.out.println("\nIn MyAuthProvider.authenticate(): ");
// Get the User from UserDetailsService
String providedUsername = authentication.getPrincipal().toString();
UserDetails user = userDetailsService.loadUserByUsername(providedUsername);
System.out.println("User Details from UserService based on username-" + providedUsername + " : " + user);
String providedPassword = authentication.getCredentials().toString();
String correctPassword = user.getPassword();
System.out.println("Provided Password - " + providedPassword + " Correct Password: " + correctPassword);
// Authenticate
// If Passwords don't match throw and exception
if(!providedPassword.equals(correctPassword))
throw new RuntimeException("Incorrect Credentials");
System.out.println("Passwords Match....\n");
// return Authentication Object
Authentication authenticationResult =
new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(), user.getAuthorities());
return authenticationResult;
}@Override
public boolean supports(Class<?> authentication) {
System.out.println("\nIn MyAuthProvider.supports(): ");
System.out.println("Checking whether MyAuthProvider supports Authentication type\n");
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}}
5. In the rest package create a class AuthController.java
In this class we define the handler for the POST “/auth/login” endpoint. We create a UsernamePasswordAuthenticationToken using the data from the request body and pass it to the authenticate method of the AuthenticationManager.
package com.example.demo.rest;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import com.example.demo.models.AuthReqBody;
import com.example.demo.models.AuthResBody;@RestController
@RequestMapping("/auth")
public class AuthController {@Autowired
private AuthenticationManager authManager;
@PostMapping("/login")
public AuthResBody authenticate(@RequestBody AuthReqBody authReqBody) {
System.out.println("Auth Details: " + authReqBody);
UsernamePasswordAuthenticationToken token = new
UsernamePasswordAuthenticationToken(
authReqBody.getUsername(),
authReqBody.getPassword());
System.out.println("\nAuthentication Token Before Authentication: " + token);
Authentication authResult = authManager.authenticate(token);
System.out.println();
System.out.println("Authentication Token After Authentication: " + authResult);
System.out.println();
System.out.println("Authentication Token in Security Context: " + SecurityContextHolder.getContext().getAuthentication());
System.out.println();
if(authResult.isAuthenticated())
System.out.println("User is Authenticated");
return new AuthResBody(true);
}}
6. In the service package create a class MyUserDetailsService.java
In this class we override the loadUserByUsername method and return a UserDetails object. In this example, we return a hard-coded user but here is where you would fetch the user from database. If the username is incorrect we throw a UsernameNotFoundException.
package com.example.demo.service;import java.util.ArrayList;import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;@Service
public class MyUserDetailsService implements UserDetailsService {public MyUserDetailsService() {
}@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(!username.equals("sayan"))
throw new UsernameNotFoundException("User with username - " + username + " not found");
return new User("sayan", "password", new ArrayList<>());
}}
Testing
Run the DemoApplication.java ( as a Java Application ).
Your console should look like this.
Open Postman or any REST Client. Make a POST request to http://localhost:8080/auth/login with the following JSON request body:
{"username": "sayan","password": "password"}
You should receive the following response :
{"authenticated": true}
Your console should show all this information :
It shows
- UsernamePasswordAuthenticationToken data before and after authentication,
- UserDetails data
- SecurityContext data, etc..
Try sending a wrong username and then a correct username + wrong password and check the responses.
Get the entire source code here —https://github.com/senshiii/Spring-Security-Authentication-Flow.
Don’t forget to star the repository 😉❤
AND YAYY !!!! You have SUCCESSFULLY implemented the authentication flow of Spring Security. Congratulations !
Conclusion
Phew !! That was a lot .. Congratulations and thanks for reading till the end. Hope you liked my article. Do consider liking and sharing the article and dropping your honest review in the comments. If you have any doubt do drop a comment. Until next time. ❤
Support
If you liked my blog and it helped you in any way, do consider helping me by buying me a coffee ☕. It would mean to the moon 🌙and back to me. Thanks ❤
Other Posts by me
- Demystifying the Folder Structure of a React App ( 58k+ views )
2. React Redux Deep Dive: State Management in React with Redux