Spring Boot: JWT Authentication with Spring Security

Mar 24, 2023
Spring Boot Java

In this article, we’ll use Spring Security to secure a Spring Boot application.

We’ll use our demo app Stampy, a simple stamp-collecting app, to demonstrate securing an app. Stampy has a /stamps endpoint that can currently be accessed by anyone and we’d like to make it so that only users that have signed up can access /stamps. The secured /stamps request will look like the diagram below i.e. a token is required to authenticate the user. The token is a JSON Web Token, JWT, we’ll talk more about this in a mo.

Get stamps flow

We’ll also create /signup and /login endpoints to create a user and get a new token for an existing user respectively. the login request is depicted below.

Login flow

Check out the full example on GitHub https://github.com/minibuildsio/stampy.

Required Dependencies

We’ll need the spring-boot-starter-security library which provides security features. com.auth0:java-jwt is a library for creating JWTs others are available so feel free to swap out for an alternative if preferred, changes will be isolated to the JwtHelper class.

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.auth0:java-jwt:4.3.0'

What are JWTs

A JSON Web Token, JWT, is a JSON object containing information about a user e.g. the username and the roles they have. A JWT is signed using a secret and a cryptographic algorithm e.g. HMAC which allows you to determine that you created the token and is therefore valid.

JWTs expire after a set amount of time which can be anything from minutes to days or even longer depending on the use case. In general shorter lifespans are advised due to the limited time window an attacker would have if they got access to a user’s JWT. Short-lived JWTs are sometimes accompanied by a longer-lived refresh token which allows the user to request new JWTs.

Creating a JWT

Below is the JwtHelper class which is responsible for creating JWTs and parsing them from a string. The class requires a secret used for signing the JWT.

Most of the heavy lifting, creating, signing, and verifying the JWT, is done by the com.auth0:java-jwt library. The cryptographic algorithm used to sign the JWT is HMAC-512, the strength of which depends on the length of the secret so a longer secret is recommended.

@Component
public class JwtHelper {
  private static final String ROLES = "roles";

  private final byte[] jwtSecret;
  private final long jwtLifeSpan;

  public JwtHelper(
    @Value("${jwt.secret:a-secret}") String jwtSecret,
    @Value("${jwt.life-span:86400}") long jwtLifeSpan
  ) {  
    this.jwtSecret = jwtSecret.getBytes();
    this.jwtLifeSpan = jwtLifeSpan;
  }

  public String createJwt(String userId, List<String> roles) {
    return JWT.create()
        .withSubject(userId)
        .withArrayClaim(ROLES, roles.toArray(new String[0]))
        .withExpiresAt(new Date(System.currentTimeMillis() + jwtLifeSpan * 1000))
        .sign(HMAC512(this.jwtSecret));
  }

  public User extractUser(String token) {
    DecodedJWT jwt = JWT.require(Algorithm.HMAC512(this.jwtSecret))
        .build()
        .verify(token);

    List<SimpleGrantedAuthority> roles = jwt.getClaim(ROLES).asList(String.class).stream()
        .map(SimpleGrantedAuthority::new)
        .toList();
        
    return new User(jwt.getSubject(), "", roles);
  }
}

Configuring Spring Security

Spring Security uses a security filter chain to process incoming requests. Filters can be configured to for example determine whether an endpoint can be accessed or set up the security context depending on the information in the request.

Securing Endpoints

The configuration below creates a filter chain that allows unauthenticated access to /login and /signup because these are the endpoint used for authentication and restricts access to all other endpoints. It also adds JwtFilter to the filter chain which is responsible for extracting and verifying the token from the request.

@Configuration
public class SecurityConfig {

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http, JwtFilter jwtFilter)
    throws Exception {

    http.csrf().disable()
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login", "/signup").permitAll()
            .anyRequest().authenticated()
        )
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
  }
}

Extract the JWT from the Request Header and Set Up the Security Context

The JwtFilter class extracts the JWT from the Authorization header of the request and sets the authentication of the security context using it. The security context stores the details of the user for the current request.

@Component
public class JwtFilter extends OncePerRequestFilter {

  public static final String AUTHORIZATION = "Authorization";
  public static final String BEARER = "Bearer ";

  private final JwtHelper jwtHelper;

  public JwtFilter(JwtHelper jwtHelper) {
    this.jwtHelper = jwtHelper;
  }

  @Override
  protected void doFilterInternal(
      HttpServletRequest request,
      HttpServletResponse response,
      FilterChain filterChain
    ) throws ServletException, IOException {

    String header = request.getHeader(AUTHORIZATION);

    if (header == null || !header.startsWith(BEARER)) {
      filterChain.doFilter(request, response);
      return;
    }

    User user = this.jwtHelper.extractUser(header.replace(BEARER, ""));

    Authentication authentication = new UsernamePasswordAuthenticationToken(
      user, null, user.getAuthorities()
    );
    SecurityContextHolder.getContext().setAuthentication(authentication);

    filterChain.doFilter(request, response);
  }
}

Login Endpoint to Retrieve a JWT

For this demo, we’ll add a dummy /login endpoint that returns a JWT regardless of the details provided. In a real application, the details would be checked against details in a database.

@RestController
public class AuthController {

  private final JwtHelper jwtHelper;

  public AuthController(JwtHelper jwtHelper) {
    this.jwtHelper = jwtHelper;
  }

  @PostMapping("/login")
  public AuthResponse login(@RequestBody AuthRequest authRequest) {
    // In a real app you'd check the user details via a database query/request to a service
    String token = jwtHelper.createJwt(authRequest.username(), List.of());
    return new AuthResponse(token);
  }
}

Where AuthRequest and AuthResponse are records with fields like so:

public record AuthRequest(String username, String password) {
}

public record AuthResponse(String token) {
}

Using the JWT in a Request

We can now retrieve a JWT from the /login endpoint and use it to make a request to the /stamps endpoint by passing the token in the Authorization header.

curl -H "Content-Type: application/json" http://localhost:8080/login \
  --data '{ "username": "minibuilds", "password": "ducky" }'
# returns { "token": ... }

curl -H "Authorization: Bearer ..." http://localhost:8080/stamps
# [ {...}, {...} ]