JWT Authentication & Authorization in a Spring Boot 3 application

In this blog, we will explore the concept of JSON Web Tokens (JWT) and how they can be effectively utilized for authentication and authorization in a Spring Boot 3 application. JWTs provide a secure way to transmit information between parties, enabling stateless communication and reducing the need for session management on the server side. We will cover the fundamental principles of JWT, its structure, and how it enhances security in web applications.

Additionally, we will walk through the implementation process step-by-step, starting from setting up a new Spring Boot application to securing endpoints using Spring Security. We will discuss the necessary dependencies, create utility classes for token generation and validation, and implement authentication flows. By the end of this blog, you will have a clear understanding of how to integrate JWT into your Spring Boot applications, ensuring secure user authentication and authorization.

What is JWT?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way to securely transmit information between parties as a JSON object. JWTs are commonly used for authentication and information exchange in web applications. A JWT typically consists of three parts: the header, the payload, and the signature.

  • Header: Contains metadata about the token, such as the type (JWT) and the signing algorithm (e.g., HMAC SHA-256).

  • Payload: Contains the claims or information about the user, such as user ID, roles, and expiration time.

  • Signature: Ensures the integrity and authenticity of the token.

What is JWT Signature?

The JWT signature is a crucial component that verifies the integrity and authenticity of the token. It is created by taking the encoded header and payload, then signing them with a secret key or a public/private key pair using a hashing algorithm.

The signature serves two main purposes:

  • Integrity: It ensures that the token has not been altered. If the signature is valid upon verification, it confirms that the header and payload remain unchanged.

  • Authentication: It verifies the identity of the sender. Only the issuer of the token (who has the secret key) can generate a valid signature, preventing unauthorized parties from creating their own tokens.

In summary, the JWT signature is essential for maintaining secure authentication and authorization in applications, protecting both the token's integrity and the user's identity.

Prerequisites

Before we dive into the implementation, ensure you have the following:

  • Java Development Kit (JDK) 17 or higher

  • Maven or Gradle for dependency management

  • Basic knowledge of Spring Boot and RESTful APIs

  • An IDE (like IntelliJ IDEA or Eclipse) for coding

  • Database

Implementation Steps

  • check below pom.xml and add spring Web, Spring Security, Spring Data JPA, MySQL Driver and JSON Web Token dependencies.

      <?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>3.3.1</version>
              <relativePath/> <!-- lookup parent from repository -->
          </parent>
          <groupId>com.hk</groupId>
          <artifactId>springSecurity-boot3.x</artifactId>
          <version>0.0.1-SNAPSHOT</version>
          <name>springSecurity-boot3.x</name>
          <description>Demo project for Spring Boot</description>
          <url/>
          <licenses>
              <license/>
          </licenses>
          <developers>
              <developer/>
          </developers>
          <scm>
              <connection/>
              <developerConnection/>
              <tag/>
              <url/>
          </scm>
          <properties>
              <java.version>17</java.version>
          </properties>
          <dependencies>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-data-jpa</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-security</artifactId>
              </dependency>
    
              <dependency>
                  <groupId>com.mysql</groupId>
                  <artifactId>mysql-connector-j</artifactId>
                  <scope>runtime</scope>
              </dependency>
              <dependency>
                  <groupId>org.projectlombok</groupId>
                  <artifactId>lombok</artifactId>
                  <optional>true</optional>
              </dependency>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-test</artifactId>
                  <scope>test</scope>
              </dependency>
              <dependency>
                  <groupId>io.jsonwebtoken</groupId>
                  <artifactId>jjwt-api</artifactId>
                  <version>0.11.5</version>
              </dependency>
              <dependency>
                  <groupId>io.jsonwebtoken</groupId>
                  <artifactId>jjwt-impl</artifactId>
                  <version>0.11.5</version>
              </dependency>
              <dependency>
                  <groupId>io.jsonwebtoken</groupId>
                  <artifactId>jjwt-jackson</artifactId>
                  <version>0.11.5</version>
            </dependency>
          </dependencies>
    
          <build>
              <plugins>
                  <plugin>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-maven-plugin</artifactId>
                      <configuration>
                          <excludes>
                              <exclude>
                                  <groupId>org.projectlombok</groupId>
                                  <artifactId>lombok</artifactId>
                              </exclude>
                          </excludes>
                      </configuration>
                  </plugin>
              </plugins>
          </build>
    
      </project>
    
  • an user entity implementing "org.springframework.security.core.userdetails.UserDetails" and override required methods.

      package com.hk.sec.prep.entity;
    
      import java.util.Collection;
      import java.util.List;
    
      import org.springframework.security.core.GrantedAuthority;
      import org.springframework.security.core.authority.SimpleGrantedAuthority;
      import org.springframework.security.core.userdetails.UserDetails;
    
      import jakarta.persistence.Entity;
      import jakarta.persistence.GeneratedValue;
      import jakarta.persistence.GenerationType;
      import jakarta.persistence.Id;
      import jakarta.persistence.Table;
      import lombok.Data;
    
      @Data
      @Entity
      @Table(name="user")
      public class User implements UserDetails {
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Integer uId;
    
          private String firstName;
          private String lastName;
          private String email;
          private String password;
    
          private Role role;
    
          @Override
          public Collection<? extends GrantedAuthority> getAuthorities() {
              return List.of(new SimpleGrantedAuthority(role.name()));
          }
    
          @Override
          public String getUsername() {
              return this.email;
          }
    
          @Override
          public boolean isAccountNonExpired() {
              return true;
          }
    
          @Override
          public boolean isAccountNonLocked() {
              return true;
          }
    
          @Override
          public boolean isCredentialsNonExpired() {
              return true;
          }
    
          @Override
          public boolean isEnabled() {
              return true;
          }
    
      }
    
  • an service class with a method returning an object of "org.springframework.security.core.userdetails.UserDetailsService" and override required methods.

      package com.hk.sec.prep.services;
    
      import org.springframework.security.core.userdetails.UserDetailsService;
    
      public interface UserService {
    
          public UserDetailsService userDetailsService();
    
      }
    
      package com.hk.sec.prep.services.impl;
    
      import org.springframework.beans.factory.annotation.Autowired;
      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;
    
      import com.hk.sec.prep.repository.UserRepo;
      import com.hk.sec.prep.services.UserService;
    
      import lombok.RequiredArgsConstructor;
    
      @Service
      @RequiredArgsConstructor
      public class UserServiceImpl implements UserService{
    
          @Autowired
          private UserRepo userRepo;
    
          @Override    
          public UserDetailsService userDetailsService() {
              return new UserDetailsService() {
    
                  @Override
                  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                      return userRepo.findByEmail(username).orElseThrow(() -> new UsernameNotFoundException("User Not Found"));
    
                  }
              };
          }
    
      }
    
  • create a JWT service class to generate token ,validate token and extract user name from token using a secret key (confidential string used in the signing process of a JWT to ensure that only authorized parties can create and verify the token).

      package com.hk.sec.prep.services.impl;
    
      import java.security.Key;
      import java.util.Date;
      import java.util.Map;
      import java.util.function.Function;
    
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.stereotype.Service;
    
      import com.hk.sec.prep.services.JWTService;
    
      import io.jsonwebtoken.Claims;
      import io.jsonwebtoken.Jwts;
      import io.jsonwebtoken.SignatureAlgorithm;
      import io.jsonwebtoken.io.Decoders;
      import io.jsonwebtoken.security.Keys;
    
      @Service
      public class JWTServiceImpl implements JWTService {
    
          @Value("${security.jwt.secret-key}")
          private String secretKey;
    
          public String generateToken(UserDetails userDetails) {
              return Jwts.builder().setSubject(userDetails.getUsername()).setIssuedAt(new Date(System.currentTimeMillis()))
                      .setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
                      .signWith(getSignKey(), SignatureAlgorithm.HS256).compact();
          }
    
          public String generateRefreshToken(Map<String,Object> extraClaims, UserDetails userDetails) {
              return Jwts.builder().setClaims(extraClaims)
                      .setSubject(userDetails.getUsername())
                      .setIssuedAt(new Date(System.currentTimeMillis()))
                      .setExpiration(new Date(System.currentTimeMillis() + 604800000)) // 30 days
                      .signWith(getSignKey(), SignatureAlgorithm.HS256)
                      .compact();
          }
    
          public String extractUserName(String token) {
              return extractClaim(token, Claims::getSubject);
          }
    
          public boolean isTokenValid(String token, UserDetails userDetails) {
              //System.out.println("JWTServiceImpl.isTokenValid()" + token);
              final String userName = extractUserName(token);
              return userName.equals(userDetails.getUsername()) && !isTokenExpired(token);
          }
    
          private boolean isTokenExpired(String token) {
              // TODO Auto-generated method stub
              return extractClaim(token, Claims::getExpiration).before(new Date());
          }
    
          private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
              final Claims claims = extractAllClaims(token);
              return claimsResolver.apply(claims);
          }
    
          private Claims extractAllClaims(String token) {
              //System.out.println("JWTServiceImpl.extractAllClaims()"+token);
              return Jwts.parserBuilder().setSigningKey(getSignKey()).build().parseClaimsJws(token).getBody();
          }
    
          private Key getSignKey() {
              //System.out.println("JWTServiceImpl.getSignKey() "+  secretKey);
              byte[] key = Decoders.BASE64.decode(secretKey);
              return Keys.hmacShaKeyFor(key);
          }
    
      }
    

    what are claims ?

    Claims are pieces of information embedded in a JSON Web Token (JWT) that convey specific details about the user or the context of the token.

    Below are some predefined registered claims:

    1. iss (Issuer): Identifies the principal that issued the JWT.

    2. sub (Subject): Identifies the subject of the JWT, typically the user.

    3. aud (Audience): Identifies the recipients that the JWT is intended for.

    4. exp (Expiration Time): Indicates when the token will expire.

    5. nbf (Not Before): Indicates the time before which the token must not be accepted for processing.

    6. iat (Issued At): Indicates when the token was issued.

  • Create a JWT Filter

    What is filter in spring boot?

    In Spring boot a filter is a java class that intercepts and process HTTP requests and responses before they reach the controller or after they leave the controller.

    Why JWT Filter ?

    A JWT Filter is helpful to intercept HTTP requests to check for JSON Web Tokens (JWTs) used for authentication and authorization. It validates the presence and integrity of the token by checking its signature and expiration, ensuring that only requests with valid tokens can access protected resources. By centralizing this authentication logic, the JWTFilter enhances security, simplifies user permission management, and supports a stateless architecture, allowing the application to scale efficiently without relying on server-side session data.

    How to implement JWT Filter ?

    1. Extend org.springframework.web.filter.OncePerRequestFilter, which ensures that the filter is applied only once per request, preventing duplicate processing.

    2. Override doFilterInternal

    3. in doFilterInternal check if Bearer token exists in the request or not, if not then return.

    4. if token exists, extract username from token and check if the username is not null and if there’s no existing authentication in the security context. This ensures that the filter processes the request only if the user is not already authenticated.

    5. if user is not already authenticated (no existing authentication in the security context) then load the user details using the UserDetailsService based on the extracted username.

    6. Then validate the token.

    7. If the token is valid, a UsernamePasswordAuthenticationToken is created and set in the security context, allowing the application to recognize the user as authenticated.

    8. Finally, the filterChain.doFilter(request, response) call is made to continue processing the request, passing control to the next filter or handler in the chain.

    package com.hk.sec.prep.config;

    import java.io.IOException;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContext;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
    import org.springframework.stereotype.Component;
    import org.springframework.web.filter.OncePerRequestFilter;

    import com.hk.sec.prep.services.JWTService;
    import com.hk.sec.prep.services.UserService;

    import io.micrometer.common.util.StringUtils;
    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import lombok.RequiredArgsConstructor;

    @Component
    @RequiredArgsConstructor
    public class JWTAuthFilter extends OncePerRequestFilter {


        @Autowired
        private final JWTService jwtService;


        @Autowired
        private final UserService userService;

        /**
         * The main method that gets called for every request 
         */
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {

            // Get the Authorization header from the request
            final String authHeader = request.getHeader("Authorization");

            // If the Authorization header is missing or doesn't start with "Bearer ", proceed with the next filter
            if (authHeader == null || !authHeader.startsWith("Bearer ")) {
                filterChain.doFilter(request, response);
                return;
            }

            try {
                // Extract the JWT token from the Authorization header
                final String jwt = authHeader.substring(7);
                // Extract the username (or email) from the JWT token
                final String userEmail = jwtService.extractUserName(jwt);

                // If the username is not empty and there is no current authentication in the security context
                if (StringUtils.isNotEmpty(userEmail) && SecurityContextHolder.getContext().getAuthentication() == null) {
                    // Load user details using the username
                    UserDetails userDetails = userService.userDetailsService().loadUserByUsername(userEmail);

                    // Validate the JWT token
                    if (jwtService.isTokenValid(jwt, userDetails)) {
                        // Create an empty SecurityContext
                        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();

                        // Create a UsernamePasswordAuthenticationToken with user details and authorities
                        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());

                        // Set the details of the authentication token with the request details
                        usernamePasswordAuthenticationToken
                                .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                        // Set the authentication in the security context
                        securityContext.setAuthentication(usernamePasswordAuthenticationToken);
                        SecurityContextHolder.setContext(securityContext);
                    }
                }
            } catch (Exception e) {
                // Print the stack trace if there is an exception during the token validation process
                e.printStackTrace();
            }

            // Proceed with the next filter in the filter chain
            filterChain.doFilter(request, response);
        }
    }
  • Create a Spring security configuration class as below.

package com.hk.sec.prep.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.hk.sec.prep.entity.Role;
import com.hk.sec.prep.services.UserService;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SpringSecurityConfiguration {


    @Autowired
    private final JWTAuthFilter jwtAuthFilter;

    @Autowired
    private final UserService userService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf(AbstractHttpConfigurer::disable)
         .authorizeHttpRequests(request -> request
                    .requestMatchers("/public/**").permitAll()
                    .requestMatchers("/private/admin/**").hasAuthority(Role.ADMIN.name())
                    .requestMatchers("/private/user/**").hasAnyAuthority(Role.USER.name(),Role.ADMIN.name())
                    .anyRequest().authenticated())
         .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
         .authenticationProvider(authenticationProvider())
         .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();

    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userService.userDetailsService());
        daoAuthenticationProvider.setPasswordEncoder(PasswordEncoder());
        return daoAuthenticationProvider;
    }

    @Bean
    public PasswordEncoder PasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

}
  • Create your controllers to login and authentication and authorization. refer below code for better understanding.

      package com.hk.sec.prep.controller;
    
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.http.ResponseEntity;
      import org.springframework.web.bind.annotation.CrossOrigin;
      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.hk.sec.prep.dto.LogInResponse;
      import com.hk.sec.prep.dto.LoginRequest;
      import com.hk.sec.prep.dto.RefreshTokenRequest;
      import com.hk.sec.prep.dto.SignUpRequest;
      import com.hk.sec.prep.entity.User;
      import com.hk.sec.prep.services.AuthenticationService;
    
      @RestController
      @RequestMapping("/public/auth")
      @CrossOrigin(origins = "http://localhost:4200")
      public class AuthController {
    
          @Autowired
          private AuthenticationService authenticationService;
    
          @PostMapping("/signUp")
          public ResponseEntity<User> signUp(@RequestBody SignUpRequest signUpRequest) {
              return ResponseEntity.ok(authenticationService.signUp(signUpRequest));
    
          }
    
          @PostMapping("/logIn")
          public ResponseEntity<LogInResponse> signUp(@RequestBody LoginRequest loginRequest) {
              return ResponseEntity.ok(authenticationService.logIn(loginRequest));
    
          }
    
          @PostMapping("/refreshToken")
          public ResponseEntity<LogInResponse> refreshToken(@RequestBody RefreshTokenRequest refreshTokenRequest) {
              return ResponseEntity.ok(authenticationService.refreshToken(refreshTokenRequest));
    
          }
    
      }
    
  • The Complete code: https://github.com/hemantjava96/springSecurity-JWT-boot3.x

Did you find this article valuable?

Support Java Blogs By Hemant by becoming a sponsor. Any amount is appreciated!