← Back to Blog

JWT Authentication in Spring Boot + React: Complete Guide

Authentication is the backbone of any secure application. In this comprehensive guide, I'll walk you through implementing JWT (JSON Web Token) authentication in a Spring Boot backend with a React frontend—the exact pattern I've used at Amazon, Agoda, and multiple startups.

Why JWT for Modern Web Applications?

After building authentication systems for applications handling millions of users, I've learned that JWT strikes the perfect balance between security, scalability, and simplicity. Here's why:

  • Stateless: No server-side session storage needed, perfect for distributed systems
  • Scalable: Tokens contain all necessary user information, enabling horizontal scaling
  • Cross-domain: Works seamlessly across different domains and microservices
  • Mobile-friendly: Same token mechanism works for web and mobile apps

Architecture Overview

Our authentication flow follows industry best practices with two types of tokens:

  • Access Token: Short-lived (15 minutes), used for API requests
  • Refresh Token: Long-lived (7 days), used to obtain new access tokens
 User Login Flow:
1. User submits credentials (email + password)
2. Server validates credentials
3. Server generates access token (15 min) + refresh token (7 days)
4. Tokens sent to client
5. Client stores access token in memory, refresh token in httpOnly cookie
6. Client includes access token in Authorization header for API calls
7. When access token expires, client uses refresh token to get new one

Backend Implementation: Spring Boot

1. Dependencies (pom.xml)

 <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>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. JWT Utility Class

 @Component
public class JwtUtil {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.access-token-expiration}")
    private Long accessTokenExpiration;
    
    public String generateAccessToken(String username) {
        return Jwts.builder()
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + accessTokenExpiration))
            .signWith(getSigningKey(), SignatureAlgorithm.HS256)
            .compact();
    }
    
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }
    
    public boolean isTokenValid(String token, String username) {
        return username.equals(extractUsername(token)) && !isTokenExpired(token);
    }
    
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }
    
    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

Frontend Implementation: React

API Service with Axios

import axios from 'axios';

const API_BASE_URL = 'http://localhost:8080/api';
let accessToken = null;

const api = axios.create({
  baseURL: API_BASE_URL,
  withCredentials: true,
});

api.interceptors.request.use(
  (config) => {
    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  }
);

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      const { data } = await axios.post(
        `${API_BASE_URL}/auth/refresh`,
        {},
        { withCredentials: true }
      );
      accessToken = data.accessToken;
      return api(error.config);
    }
    return Promise.reject(error);
  }
);

export default api;

Security Best Practices

1. Token Storage

  • Access Token: Store in memory (JavaScript variable), never localStorage
  • Refresh Token: Store in httpOnly cookie with Secure and SameSite flags
  • Why? Prevents XSS attacks from stealing tokens

2. Token Expiration

  • Short-lived access tokens (15 minutes) limit damage if compromised
  • Longer ref