Friday, September 15, 2023

Demystifying Role based JWT Authentication in Modern Web Applications using spring boot

  • JWT-based authentication is a popular method for securing web applications and APIs. JWT stands for "JSON Web Token," and it is a compact and self-contained way to represent information between two parties. In the context of authentication, JWTs are used to verify the identity of a user.

in-depth explanation of JWT-based authentication:

JSON Web Token (JWT):

  • A JWT is a JSON object encoded as a single string. It consists of three parts: a header, a payload, and a signature. These parts are concatenated with dots (.) to form a compact, URL-safe string. Here's what each part does:
  • Header: Contains metadata about the type of token and the hashing algorithm used.
  • Payload: Contains claims or statements about an entity (typically, the user) and additional data.
  • Signature: Ensures the integrity of the token. It's created by signing the header and payload with a secret key.

How JWT-Based Authentication Works:

  • ๐Ÿง‘‍๐Ÿ’ผ User Authentication:
    •  A user provides their credentials (e.g., username and password) to the server during login.
  • ๐ŸŽซ Token Generation
    • Upon successful authentication, the server creates a JWT containing information about the user (claims), such as their user ID, roles, and other relevant data. This information is stored in the token's payload.
  • ๐Ÿ–‹️ Token Signing
    • The server signs the JWT using a secret key. This signature ensures that the token hasn't been tampered with.
  • ๐Ÿ“จ Token Response
    • The server sends the JWT to the client, typically in an HTTP response, as part of the login response.
  • ๐Ÿ’ผ Client-Side Storage
    • The client (usually a web browser or mobile app) stores the JWT securely. Common storage locations include browser local storage, session storage, or cookies.
  • ๐Ÿ” Authentication Requests
    • For subsequent requests to protected resources, the client includes the JWT in the request headers, usually using the Authorization header with the Bearer prefix (e.g., Authorization: Bearer <token>).
  • ๐Ÿงพ Token Verification: 
    • The server receiving the request verifies the JWT's authenticity by checking the signature using the secret key. It also checks the token's expiration time and other claims.
  • ๐Ÿšช Access Control
    • If the JWT is valid and contains the required claims (e.g., proper user role), the server allows access to the requested resource.   

Advantage of JWT-Based Authentication 

  • ๐Ÿš€ Compact and Self-Contained: 
    • JWTs are compact, URL-safe tokens encoded as a string. They consist of three parts: a header, a payload, and a signature. These parts are typically base64-encoded and separated by periods.
  • ๐Ÿ”‘ Stateless: 
    • JWT-based authentication is stateless, meaning the server doesn't need to store session data for each user. This makes it highly scalable and suitable for microservices architectures.
  • ๐Ÿ” Cross-Domain: 
    • JWTs can be used across different domains or services, making them suitable for single sign-on (SSO) scenarios.
  • ๐Ÿ›ก️ Tamper-Proof: 
    • The signature in a JWT ensures the integrity of the token. Any tampering with the token is easily detectable.

Step 1: Set up a Spring Boot Project

  • Use Spring Initializer (https://start.spring.io/) or your IDE to create a Spring Boot project with the necessary dependencies: Spring Web, Spring Security, and Spring Data JPA (optional)

Step 2: Configure Spring Security

  • Create a custom CustomUserDetails class that implements UserDetails. This class represents user information.
  • secret-key: This is a secret key used for JWT signing and verification.
  • expiration-time: This specifies the expiration time for JWT tokens.
  • spring: security: jwt: secret-key: mySecretKey expiration-time: 86400000 # 1 day in milliseconds

Step 3: Add Dependencies

  • In your pom.xml (if using Maven) or build.gradle (if using Gradle), add the following dependencies.
  • <!-- For Spring Boot Web and Security -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- For JWT support --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>

  • spring-boot-starter-web: Provides the necessary dependencies for building web applications.
  • spring-boot-starter-security: Adds Spring Security support.
  • jwt: The JSON Web Token library for working with JWTs.
  • These dependencies are essential for creating a Spring Boot application with JWT-based security.

Step 4: Create a Security Configuration Class

  • Create a security configuration class that extends WebSecurityConfigurerAdapter. Configure Spring Security to use JWT authentication and define role-based access control using in-memory user data.
    • @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/public").permitAll() // Public endpoint .antMatchers("/api/user").hasRole("USER") // Requires USER role .antMatchers("/api/admin").hasRole("ADMIN") // Requires ADMIN role .anyRequest().authenticated() // Other endpoints require authentication .and() .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // Configure in-memory user data with roles auth.inMemoryAuthentication() .withUser("user").password(passwordEncoder().encode("password")).roles("USER") .and() .withUser("admin").password(passwordEncoder().encode("admin")).roles("ADMIN"); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
         In this configuration:
    • WebSecurityConfigurerAdapter. This class configures Spring Security to use JWT authentication and defines role-based access control
    • .csrf().disable(): Disables Cross-Site Request Forgery (CSRF) protection. In a REST API, CSRF is generally not applicable.
      • .authorizeRequests(): Begins configuring access control rules.
      • .antMatchers("/api/public").permitAll(): Grants public access to the /api/public endpoint.
      • .antMatchers("/api/user").hasRole("USER"): Requires users to have the "USER" role to access /api/user.
      • .antMatchers("/api/admin").hasRole("ADMIN"): Requires users to have the "ADMIN" role to access /api/admin.
      • .anyRequest().authenticated(): Requires authentication for all other endpoints.
    • .exceptionHandling(): Configures how exceptions related to authentication are handled.
    • .sessionManagement(): Configures session management, specifying STATELESS to indicate stateless (token-based) authentication.
    • The configure(AuthenticationManagerBuilder auth) method configures authentication using in-memory users with roles. This is useful for demonstration purposes but should be replaced with a database-backed user system in a production environment.

    Step 5: Create JwtAuthenticationFilter

    • Create a custom JwtAuthenticationFilter class to validate JWT tokens and check the user's roles when processing incoming requests.
      • @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private CustomUserDetailsService customUserDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // Extract JWT token from the Authorization header String jwtToken = extractJwtToken(request); if (jwtToken != null && jwtUtil.validateToken(jwtToken)) { // Extract username from the JWT token String username = jwtUtil.extractUsername(jwtToken); // Load user details from the database based on the username CustomUserDetails userDetails = customUserDetailsService.loadUserByUsername(username); // Check if the user has the required role to access the endpoint List<String> requiredRoles = getRequiredRoles(request); if (userDetails.getRoles().containsAll(requiredRoles)) { // User has the required roles, so set the user as authenticated UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException e) { // Handle JWT exceptions, e.g., token expired or invalid signature // You can customize the error handling based on your requirements // For example, you can send a 401 Unauthorized response } filterChain.doFilter(request, response); } private String extractJwtToken(HttpServletRequest request) { // Extract the token from the Authorization header (Bearer token) String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { return header.substring(7); // Remove "Bearer " prefix } return null; } private List<String> getRequiredRoles(HttpServletRequest request) { // Determine the required roles based on the request URL or other criteria // You can customize this logic to match your application's requirements List<String> requiredRoles = new ArrayList<>(); String requestURI = request.getRequestURI(); if (requestURI.startsWith("/api/user")) { requiredRoles.add("USER"); } else if (requestURI.startsWith("/api/admin")) { requiredRoles.add("ADMIN"); } return requiredRoles; } }
      • In this step, we create a custom JwtAuthenticationFilter class that validates JWT tokens and checks the user's roles when processing incoming requests. This filter is responsible for extracting JWTs from incoming requests, validating them, and setting the user as authenticated if the token is valid.
      • OncePerRequestFilter ensures that the filter is executed once per HTTP request.
      • The doFilterInternal method is the core logic of the filter:
      • It extracts the JWT token from the "Authorization" header of the incoming request.
      • Validates the token using the jwtUtil.
      • Loads user details based on the username extracted from the token.
      • Checks if the user has the required roles to access the requested endpoint.
      • Sets the user as authenticated if all conditions are met.
      • JWT-related exceptions, such as expired tokens or invalid signatures, are caught and can be handled based on your application's requirements

      Step 6: Create CustomUserDetailsService

      • Create a CustomUserDetailsService class to load user details from the database based on the username. Ensure that you populate user roles when loading user details.
        • @Service public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // You can implement database or repository access here, but for in-memory data, we'll create it directly. if ("user".equals(username)) { return User.withUsername("user") .password(passwordEncoder().encode("password")) .roles("USER") .build(); } else if ("admin".equals(username)) { return User.withUsername("admin") .password(passwordEncoder().encode("admin")) .roles("ADMIN") .build(); } else { throw new UsernameNotFoundException("User not found with username: " + username); } } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
        • This step involves creating a CustomUserDetailsService class that implements the UserDetailsService interface. The UserDetailsService interface is part of Spring Security and provides a method to load user details based on a username.
        • @Service marks this class as a Spring service, allowing it to be automatically detected and used by Spring.
        • The loadUserByUsername method loads user details based on the username provided.
        • In this example, we use in-memory user data for simplicity. In a real-world application, you would typically load user data from a database.

        Step 7: Create REST Endpoints

        • Create a REST controller with public and private endpoints.
          • @RestController @RequestMapping("/api") public class ApiController { @GetMapping("/public") public ResponseEntity<String> getPublicData() { return ResponseEntity.ok("This is public data accessible by everyone."); } @GetMapping("/user") public ResponseEntity<String> getUserData() { return ResponseEntity.ok("This is user data. You need to be authenticated and have the USER role to access this."); } @GetMapping("/admin") public ResponseEntity<String> getAdminData() { return ResponseEntity.ok("This is aDMIN data. You need to be authenticated and have the aDMIN role to access this."); } }
          • Finally, we create REST endpoints using a Spring @RestController. In this case, we define three endpoints:
          • /api/public: A public endpoint accessible by everyone.
          • /api/user: A secured endpoint that requires the "USER" role.
          • /api/admin: A secured endpoint that requires the "ADMIN" role.
          • Each endpoint returns a simple message indicating its accessibility. These endpoints demonstrate the role-based access control implemented in the security configuration

          Conclusion

          • This is a basic example of implementing Spring Security with JWT for a REST API. In a real-world application, you would need to add more features, error handling, user management, and robust security configurations to handle production scenarios.

          You may also like

          Kubernetes Microservices
          Python AI/ML
          Spring Framework Spring Boot
          Core Java Java Coding Question
          Maven AWS