
Think of JWT like attending an exclusive multi-day conference. When we first register, we receive 2 important items:
Before JWT, web applications relied on session-based authentication. This approach stored session information on servers, creating scalability challenges as applications grew. JWT emerged as a solution by introducing stateless authentication – instead of maintaining server-side sessions, the server generates a signed token containing user information that clients store and include with each request.
A JWT consists of three distinct sections, separated by periods:
aaaa.bbbb.cccc
Think of the authentication header like a VIP pass that we show at each checkpoint - it consists of two parts: the type of pass ("Bearer") and our unique identifier (the JWT token).
The HTTP header for JWT authentication follows this format:
Authorization: Bearer aaaa.bbbb.cccc
The word "Bearer" indicates the authentication scheme, followed by a space and then the JWT token. This scheme tells the server "the bearer of this token should be granted access to the requested resources."
On the server side, we need to extract and validate the token from the Authorization header:
Copy// User entity
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
// Getters, setters, and constructors
}
// DTO for login requests
public class LoginRequest {
private String username;
private String password;
// Getters, setters
}
// DTO for authentication response
public class AuthResponse {
private String accessToken;
private String refreshToken;
// Getters, setters
}
// JWT utility class
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
}
// Authentication controller
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
String accessToken = jwtUtil.generateToken(loginRequest.getUsername());
String refreshToken = jwtUtil.generateRefreshToken(loginRequest.getUsername());
return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken));
} catch (AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
}
// Protected resource controller
@RestController
@RequestMapping("/api/protected")
public class ProtectedController {
@GetMapping("/data")
public ResponseEntity<?> getProtectedData() {
return ResponseEntity.ok("This is protected data");
}
}
// src/services/authService.js
import axios from 'axios';
const API_URL = 'http://localhost:8080/api';
const axiosInstance = axios.create({
baseURL: API_URL
});
// Add token to requests
axiosInstance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Handle token refresh
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post(`${API_URL}/auth/refresh`, { refreshToken });
localStorage.setItem('accessToken', response.data.accessToken);
return axiosInstance(originalRequest);
} catch (refreshError) {
// Redirect to login if refresh fails
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
// src/components/Login.jsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const Login = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
try {
const response = await axiosInstance.post('/auth/login', {
username,
password
});
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);
navigate('/dashboard');
} catch (error) {
setError('Invalid credentials');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8">
<form onSubmit={handleLogin} className="mt-8 space-y-6">
<div>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
className="appearance-none rounded-md relative block w-full px-3 py-2 border"
/>
</div>
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="appearance-none rounded-md relative block w-full px-3 py-2 border"
/>
</div>
{error && <div className="text-red-500">{error}</div>}
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
>
Sign in
</button>
</form>
</div>
</div>
);
};
// src/components/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
const ProtectedRoute = ({ children }) => {
const isAuthenticated = !!localStorage.getItem('accessToken');
return isAuthenticated ? children : <Navigate to="/login" />;
};
// src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
);
};
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated();
}
}
When working with Bearer tokens, keep these in mind:
The combination of proper header formatting and robust server-side validation creates a secure authentication system. When a client includes the Bearer token in their requests, it's like presenting a digital ID card that proves their identity and permissions for each operation they want to perform.
Access tokens intentionally have short lifespans (15-60 minutes) as a security measure. This brief duration limits potential damage if a token is compromised. However, relying solely on access tokens would force users to frequently log in again – creating a poor user experience.
Refresh tokens solve this usability challenge by:
@Service
public class TokenService {
@Autowired
private JwtConfig jwtConfig;
public TokenResponse refreshAccessToken(String refreshToken) {
// First, ensure the refresh token is valid
if (!isValidRefreshToken(refreshToken)) {
throw new InvalidTokenException("Refresh token is invalid or expired");
}
// Extract user information from the refresh token
String username = extractUsername(refreshToken);
UserDetails userDetails = userService.loadUserByUsername(username);
// Generate a fresh access token
String newAccessToken = jwtConfig.generateToken(userDetails);
return new TokenResponse(newAccessToken, refreshToken);
}
}
When implementing JWT authentication in Spring Boot, consider these aspects: