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
de>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)
de><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
de>@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
de>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 refresh tokens (7 days) balance security with user experience
⚠️ Don't Store Sensitive Data in JWT
JWT tokens are base64 encoded, not encrypted. Never store passwords, credit card numbers, or other sensitive information in the token payload.
⚠️ Don't Use localStorage for Tokens
localStorage is vulnerable to XSS attacks. Any malicious script can read tokens from localStorage. Use memory for access tokens and httpOnly cookies for refresh tokens.
Production Checklist
- ✅ Use strong secret key (at least 256 bits, stored in environment variables)
- ✅ Enable HTTPS and set Secure flag on cookies
- ✅ Implement rate limiting on authentication endpoints
- ✅ Add logging for failed authentication attempts
- ✅ Set up monitoring for unusual authentication patterns
- ✅ Use SameSite=Strict or SameSite=Lax on cookies
- ✅ Implement CSRF protection
- ✅ Add password strength requirements
Conclusion
JWT authentication with Spring Boot and React is a robust, scalable solution I've deployed across multiple production systems handling millions of users. The key is balancing security with user experience—short-lived access tokens for security, refresh tokens for convenience.
This pattern has served me well at Amazon's payment systems, Agoda's booking platform, and CoinSwitch's crypto exchange. When implemented correctly with the security considerations outlined above, it provides enterprise-grade authentication.
💡 Need Help Implementing Authentication?
I offer consulting services for backend architecture, security implementation, and code reviews. If you're building a system that needs production-grade authentication, let's talk.