Exploring the Inner Workings of JWT in Spring Security: An In-depth Analysis

Overview

Every HTTP communication begins with making a request and ends with returning a response. However, giving unrestricted access to your APIs can compromise your application's security, leaving it vulnerable to unauthorized clients without the necessary permissions. An in-depth understanding of security architecture empowers you to tailor your project to meet your specific requirements, such as distinguishing between an admin and a regular user. It's important to note that, when using tokens to enhance your application's security, authentication endpoints like login and register typically do not require authorization tokens. These endpoints serve as the entry points into the application, and the tokens are generated automatically in response to the requests made to these endpoints.

This article delves into the intricacies of JWT (JSON Web Token) Spring Security architecture. Its primary aim is to provide a comprehensive understanding of the internal workings of this framework, which plays a pivotal role in securing your application's APIs.

1.0 JwtAuthFilter

The JwtAuthFilter serves as the entry point of the Spring Security process, intercepting every incoming request. Initially, when an HTTP request is received, the filter checks for the existence of a token passed along with the request. If no token is found or empty, the response will return an HTTP 403 error code, signalling a failed authentication. If a valid token is present, the JwtFilter, an interface, is invoked to extract the user's username from the token. This extracted username is then passed as an argument to the UserDetailsService for validation against the database to verify the user's existence.

2.0 UserDetailsService

The UserDetailsService is a pivotal class responsible for leveraging the database to validate the presence of a user by utilizing the extracted username. It achieves this through the loadByUsername() method, which retrieves user details that correspond to the provided username. The UserDetailsService distinguishes between a current user and a new user, returning a user detail object that encapsulates relevant information. In the event of no matching user being found, the UserDetailsService sends an HTTP 403 error response to the client, signalling the non-existence of the user.

Upon identifying a user, the JwtService helper class conducts a validation process on the user details and the token to ascertain its eligibility for use. This essential step is commonly referred to as user authentication. An exemplary implementation of the UserDetailsService may appear as follows:


@Configuration
@RequiredArgsConstructor
public class AuthenticationConfig {
    //Has direct access to the database.
    private final UserRepository repository;

    //Using dependency injection, declare the UserDetailsService as a bean
    //and state the declaration.
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> repository.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    }
}
UserDetails userDetails = userDetailsService().loadUserByUsername(userEmail);

3.0 The JWTService Helper Class

The JWTService helper class plays a crucial role in the security and functionality of your application. It contains various helper functions that assist with:

  1. Token Generation: It assists in generating a new token when users log in or register, facilitating secure access to your application.

  2. Username Extraction: The class helps to extract essential details, including the username used by the UserDetailsService, which is vital for user identification and authentication.

  3. Token Validation: The JWTService class is responsible for thorough token validation when requests are made. This validation process includes several key checks to ensure both the token's validity and security:

    • Expiration Time Check: It examines the expiration time of the token to determine if it's still valid. An expired token is not accepted, enhancing security.

    • Issued At Time Verification: It checks the token's "Issued at" time to ascertain when the token was generated.

These checks are performed by extracting and analyzing the Claims from the token. Claims are pieces of information about a user, typically stored within a token, representing various attributes or characteristics of the user, including the username and expiration time.

If the validation check fails, access to the resource is rightfully denied. The fundamental objective here is to guarantee that both the user and the token are subjected to comprehensive validation, ensuring the overall security and integrity of the authentication process. An example of a JWTService class:

public class JWTService {

    public String extractUsername(String token){
        return extractClaim(token, Claims::getSubject);
    }

    public boolean isTokenValid(String token, UserDetails userDetails){ //checks if the token belongs to the userDetails and if the token is still valid, not expired.
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    //returns the expiration time of the token
    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    //Extract a claim for a particular user from the token, claims can include expiration time
    // and so on, information about a current user, including username, password and so on.
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver){
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

   //TODO: getSignInKey()
    private Claims extractAllClaims(String token){
        return Jwts.parserBuilder()
                .setSigningKey(getSignInKey()) //handles the signature part of the jwt and verifies the client is legit
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

4.0 SecurityContextHolder

Upon successful completion of the validation process and authorization, the SecurityContextHolder plays a pivotal role by updating the security context with the authenticated user's information. This update signifies that the user is either authenticated or not.

A crucial aspect of this process is that it returns a "true" status. This status update serves to inform the entire filter context (security context) that the user has been authenticated, which triggers an automatic dispatch of the request to the Servlet. Subsequently, it executes the response with a 200 status code, indicating a successful operation. The following code illustrates this :

//authToken is generated from the user details 
SecurityContextHolder.getContext().setAuthentication(authToken);

The security context serves as the contextual container for the current thread of execution. Within this context, vital information about the current user is encapsulated, including their username, roles (e.g., admin or user), permissions, and other relevant details. This context is designed to be accessible to various components throughout the application.

To facilitate access to the security context from different parts of the application, the SecurityContextHolder is utilized. It serves as a central repository for managing and retrieving the SecurityContext. Controllers, among other components, can easily access the security context via the SecurityContextHolder.

The context is established and populated with user-specific information following a successful login. On the other hand, it is reset and cleared when the user logs out, ensuring that security-related information remains accurate and secure throughout the user's interaction with the application. This dynamic handling of the security context is pivotal in maintaining the integrity and security of the user's session.

5.0 DispatcherServlet

This is the last stage before requests are granted. The servlet is responsible for routing incoming requests to the appropriate controller method for processing. Also, it uses request mappings to determine which controller should handle a particular request based on the URL. It also ensures that responses are handled and rendered correctly. It is a critical part of the Spring MVC and provides a way to structure and handle requests in a clean and organized manner.

Conclusion

In the ever-evolving landscape of application security, understanding the intricate mechanisms within these security frameworks is paramount. This knowledge not only empowers you to tailor them to your application's unique needs but also ensures that you stand as a vigilant guardian against threats.

While the steps outlined here provide a structured and straightforward approach to fortify your application, it's crucial to recognize that there is no one-size-fits-all solution. However, the true essence of the JWT framework lies in comprehending its fundamental principles, offering you the flexibility to adapt it seamlessly to your application's architecture.

I trust that this journey has equipped you with the confidence and knowledge to wield this security framework effectively. With your newfound insights, you're prepared to customize and fortify your application in alignment with your specific requirements.

If you found this article helpful and informative, please consider showing your appreciation by giving it a thumbs-up. Your engagement fuels my commitment to providing valuable content. Thank you for your support