In this article we will explore Spring security and will build a authentication system with OAuth 2.0.
Before diving into how Spring Security operates, it's crucial to understand the request-handling lifecycle in a Java-based web server. Spring Security seamlessly integrates into this lifecycle to secure incoming requests.
The lifecycle of handling an HTTP request in a Spring-based application with Spring Security involves several stages, each playing a critical role in processing, validating, and securing the request.
The lifecycle begins when a client (e.g., browser, mobile app, or API tool like Postman) sends an HTTP request to the server.
Example:
GET /api/admin/dashboard HTTP/1.1
The servlet container (e.g., Tomcat) receives the request and delegates it to the DispatcherServlet, the front controller in a Spring application. This is where the application’s processing pipeline starts.
Before the DispatcherServlet processes the request, Spring Security's Filter Chain intercepts it. The filter chain is a sequence of filters, each responsible for handling specific security tasks. These filters ensure the request meets authentication and authorization requirements before it reaches the application logic.
Authentication Filters:
These filters verify if the request contains valid credentials, such as a username/password, a JWT, or session cookies.
Authorization Filters:
After authentication, these filters ensure the authenticated user has the necessary roles or permissions to access the requested resource.
Other Filters:
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
If authentication is successful, Spring Security creates an Authentication object and stores it in the SecurityContext. This object, often stored in a thread-local storage, is accessible throughout the request lifecycle.
Principal: Represents the authenticated user (e.g., username).
Credentials: Includes authentication details like JWT tokens or passwords.
Authorities: Contains roles and permissions assigned to the user.
A request passes through the authentication filters.
If the credentials are valid, the Authentication object is created and added to the SecurityContext.
If the credentials are invalid, the ExceptionTranslationFilter sends a 401 Unauthorized response to the client.
Once the request successfully passes through the Spring Security Filter Chain, the DispatcherServlet takes over:
Handler Mapping:
It maps the incoming request to the appropriate controller method based on the URL and HTTP method.
Controller Invocation:
The mapped controller processes the request and returns the appropriate response, often with help from other Spring components like services and repositories.
Spring Security integrates itself into this lifecycle through its filters, intercepting requests at the earliest stage. By the time a request reaches the application logic, it has already been authenticated and authorized, ensuring only legitimate traffic gets processed by the core application.
Spring Security’s design ensures that authentication, authorization, and other security measures are handled declaratively, giving developers the flexibility to customize or extend its behavior as needed. It not only enforces best practices but also simplifies the implementation of complex security requirements in modern applications.
Having explored the Filter Chain in Spring Security, let’s delve into some other key components that play a pivotal role in the authentication and authorization process.
AuthenticationManager is an interface that defines a single method , authenticate(Authentication authentication) , which is used to verify the credentials of a user and determine if they are valid. You can think of AuthenticationManager as a coordinator where you can register multiple providers, and based on the request type, it will deliver an authentication request to the correct provider.
An AuthenticationProvider is an interface that defines a contract for authenticating users based on their credentials. It represents a specific authentication mechanism, such as username/password, OAuth, or LDAP. Multiple AuthenticationProvider implementations can coexist, allowing the application to support various authentication strategies.
Authentication Object:
The AuthenticationProvider processes an Authentication object, which encapsulates the user’s credentials (e.g., username and password).
authenticate Method:
Each AuthenticationProvider implements the authenticate(Authentication authentication) method, where the actual authentication logic resides. This method:
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
A database-backed AuthenticationProvider validates usernames and passwords.
An OAuth-based AuthenticationProvider validates tokens issued by an external identity provider.
UserDetailsService is described as a core interface that loads user-specific data in the Spring documentation It contains a single method loadUserByUsername which accepts username as parameter and returns the ==User== identity object . Basically we create and implementation class of UserDetailsService in which we override the loadUserByUsername method.
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
Now how all these three works together is AuthenticationManager will ask AuthenticationProvider to carry on the authentication according to the type of Provider specified and the UserDetailsService implementation will help the AuthenticationProvider in proving the userdetails .
The user sends a request to the authenticated endpoint with their credentials (username and password) or a JWT token (in the header) and the request is passed to the Authentication Filter
AuthenticationFilter (e.g., UsernamePasswordAuthenticationFilter):
This custom filter extends OncePerRequestFilter and is placed before the UsernamePasswordAuthenticationFilter , and what it do is it extracts the token from request and validates it.
If the token is valid it creates a UsernamePasswordAuthenticationToken and sets that token into the Security Context which tells the spring security that the request is authenticated and when this request passes to the UsernamePasswordAuthenticationFilter it just passes as it has the UsernamePasswordAuthenticationToken
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
this UsernamePasswordAuthenticationToken is generated with the help of the AuthenticationManager and AuthenticationProvider if we have passed username and password instead of the token after authenticating the username and password with the help of out UserDetails class.
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
UserDetailsService: The AuthenticationProvider uses UserDetailsService to load user details based on the username. And we provide this with a implementation of UserDetailsService
Credential Validation: It compares the provided password with the one stored in the user details (typically using a PasswordEncoder).
package com.oauth.backend.services; import com.oauth.backend.entities.User; import com.oauth.backend.repositories.UserRepository; import lombok.RequiredArgsConstructor; 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.Component; @Component public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if(user==null){ throw new UsernameNotFoundException(username); } return new UserDetailsImpl(user); } public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email); if(user==null){ throw new UsernameNotFoundException(email); } return new UserDetailsImpl(user); } }
Now all these different filters and beans are needed to be configured so that Spring security knows what to do hence we create a configuration class where we specify all the configuration.
@Component public class JWTFilter extends OncePerRequestFilter { private final JWTService jwtService; private final UserDetailsService userDetailsService; public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) { this.jwtService = jwtService; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); if(authHeader == null || !authHeader.startsWith("Bearer")) { filterChain.doFilter(request,response); return; } final String jwt = authHeader.substring(7); final String userName = jwtService.extractUserName(jwt); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(userName !=null && authentication == null) { //Authenticate UserDetails userDetails = userDetailsService.loadUserByUsername(userName); if(jwtService.isTokenValid(jwt,userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); SecurityContextHolder.getContext() .setAuthentication(authenticationToken); } } filterChain.doFilter(request,response); } }
till now we have understand and configured our authentication with the help of spring security now it’s time to test it.
We will create a simple app with two controllers AuthController (handles login and register) and ProductController (dummy protected controller)
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
package com.oauth.backend.services; import com.oauth.backend.entities.User; import com.oauth.backend.repositories.UserRepository; import lombok.RequiredArgsConstructor; 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.Component; @Component public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if(user==null){ throw new UsernameNotFoundException(username); } return new UserDetailsImpl(user); } public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email); if(user==null){ throw new UsernameNotFoundException(email); } return new UserDetailsImpl(user); } }
@Component public class JWTFilter extends OncePerRequestFilter { private final JWTService jwtService; private final UserDetailsService userDetailsService; public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) { this.jwtService = jwtService; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); if(authHeader == null || !authHeader.startsWith("Bearer")) { filterChain.doFilter(request,response); return; } final String jwt = authHeader.substring(7); final String userName = jwtService.extractUserName(jwt); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(userName !=null && authentication == null) { //Authenticate UserDetails userDetails = userDetailsService.loadUserByUsername(userName); if(jwtService.isTokenValid(jwt,userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); SecurityContextHolder.getContext() .setAuthentication(authenticationToken); } } filterChain.doFilter(request,response); } }
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception{ return config.getAuthenticationManager(); }
@Bean public AuthenticationProvider authenticationProvider(){ DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsServiceImpl); authenticationProvider.setPasswordEncoder(passwordEncoder); return authenticationProvider; }
till now we have implemented a registration , login and verification but what if i also want to add Login With Google/Github fuctionality then we can do it with the help of OAuth2.0
OAuth 2.0 is a protocol made for authorization through which enables users to grant third party applications access to resources stored on other platforms (e.g Google Drive , Github) without sharing the credentials of those platforms.
It is mostly used in enabling social logins like “Login with google” , “Login with github ” .
Platforms like Google , Facebook , Github provides Authorization server which implements OAuth 2.0 protocol for this social sign in or authorizing access .
Now we will look into each concept one by one
Resource owner is the user who wants to authorize the third party application (Your Application).
It is your (third party) application which wants to access the data or resource from the resource server.
It is the server where the user’s data is stored and is to be accessed by the third party application.
The server that authenticates the resource owner and issues access tokens to the client (e.g., Google Accounts).
A credential issued by the authorization server to the client, allowing it to access the resource server on behalf of the user. It is generally short lived that is expires very soon so a refresh token is also provided in order to refresh this access token so that user doesn’t need to authorize again.
Specific permissions granted by the user, defining what the client can and cannot do with the user's data. For example for authorization we only need user info like profile , name etc. but for file access different scope is required.
It refers to the methods by which Client application can obtain the access token from the Authorization Server. A grant defines the process and conditions under which the client application is authorized to access a resource owner's protected data.
It is secure as we do not need to expose our client secret and other credentials to the browser
There are two mostly used Grant types provided by OAuth 2.0
It is the most used type of grant/method , most secure and is for server-side applications
In this a authorization code is given by the client to the backend and the backend gives the access token to the client.
Process:
Used by single-page apps (SPAs) or applications without a backend. In this the access token is directly generated and issued in the browser itself.
Process:
We will implement both separately for complete understanding , but first we will implement the Authorization Code Grant for that we will need
Authorization Server
It can be either of a platform (like google , github) or you can create your own also using KeyCloak or can also build your own adhering to OAuth 2.0 standards ( we may do this in next blog ?)
Spring Boot Application
This will be our main backend application/service which will handle all the operations like code exchange, verification , saving user details and assigning JWT tokens
React Application (Frontend)
This will be our client which will redirect the user to Authorization Server for authorization.
So in our implementation what we will be doing is the frontend(web/app) will redirect our user to google login with redirect uri to our backend endpoint which will take control further we will talki about it later and along with the redirect_url we also be passing the client id of our app all these will be sent in the query parameters.
No when the user will successfully login in the google the authrization server(google’s) will redirect our request to the backend enpoint and there what we will be doing is exchanging Authorization code with authorization server to get access token and refresh token and then we can handle the auth as we want and finally we will send a response back to our frontend which will have a cookie and redirect to our dashboard or may be a protected page.
Now we will look into the code , but make sure that you add the url of your backend endpoint in authorized redirect urls in Google console dashboard for OAuth client.
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
and that’s it this will work fine and for testing you can made a simple frontend application which will nothing but having a context and yout know login and registration functions.
Thanks for reading till this long , and if you have any suggestion , please drop it in the comments
The above is the detailed content of Understanding Spring Security and OAuth. For more information, please follow other related articles on the PHP Chinese website!