JSON Web Token (JWT) authentication is a stateless authentication mechanism that's become increasingly popular in modern web applications. We will walk you through implementing a complete authentication system using Spring Boot for the backend and React for the frontend.
A JWT consists of three parts:
1. Header - Contains the type of token and the signing algorithm
2. Payload - Contains the claims (user data)
3. Signature - Used to verify the token hasn't been tampered with
Example JWT structure:
aaaa.
bbbb.
cccc
First, set up your Spring Boot project with the necessary dependencies in build.gradle
:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
Create the User entity class to represent users in your database:
@Entity
@Table(name = "users")
@Data
public class Users {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Integer id;
@Column(name = "created_at")
private Date created_at;
@Column(name = "email")
private String email;
@Column(name = "password_hash")
private String password_hash;
@Column(name = "username")
private String username;
@Column(name = "updated_at")
private Date updated_at;
}
This entity class:
Create a utility class to handle JWT operations:
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
Key points about the JWT utility:
Configure Spring Security to use JWT:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final CustomUserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
configuration.setExposedHeaders(List.of("Authorization"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Important security configuration aspects:
Create a filter to process JWT tokens:
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
username = jwtTokenUtil.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
The filter:
Create endpoints for authentication:
@RestController
@RequestMapping("/api/auth")
@CrossOrigin(origins = "http://localhost:3000")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenUtil jwtTokenUtil;
private final UsersRepository usersRepository;
private final PasswordEncoder passwordEncoder;
@PostMapping("/signup")
public ResponseEntity<?> signup(@RequestBody SignupRequest request) {
// Validate if user exists
if (usersRepository.findByUsername(request.getUsername()).isPresent()) {
return ResponseEntity.badRequest().body("Username already taken");
}
// Create new user
Users user = new Users();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPassword_hash(passwordEncoder.encode(request.getPassword()));
user.setCreated_at(new Date());
user.setUpdated_at(new Date());
usersRepository.save(user);
// Generate token
String jwt = jwtTokenUtil.generateToken(user.getUsername());
return ResponseEntity.ok(new AuthResponse(jwt));
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
}
String jwt = jwtTokenUtil.generateToken(request.getUsername());
return ResponseEntity.ok(new AuthResponse(jwt));
}
}
Create a service to handle API calls:
import axios from 'axios';
const API_URL = 'http://localhost:8080/api';
const api = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json'
}
});
// Add request interceptor to include JWT token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Add response interceptor to handle token expiration
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
Create an authentication context to manage auth state:
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem('token');
setIsAuthenticated(!!token);
setIsLoading(false);
}, []);
const login = (token) => {
localStorage.setItem('token', token);
setIsAuthenticated(true);
};
const logout = () => {
localStorage.removeItem('token');
setIsAuthenticated(false);
};
if (isLoading) {
return <div>Loading...</div>;
}
return (
<AuthContext.Provider value={{ isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
Create a login form component:
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import api from '../services/api';
function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const { login } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
try {
const response = await api.post('/auth/login', {
username,
password
});
login(response.data.token);
navigate('/dashboard');
} catch (err) {
setError(err.response?.data || 'An error occurred');
}
};
return (
<div className="login-container">
<form onSubmit={handleSubmit}>
{error && <div className="error">{error}</div>}
<div>
<label>Username:</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div>
<label>Password:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">Login</button>
</form>
</div>
);
}
export default Login;
User Signs Up:
User Logs In:
Making Authenticated Requests:
Token Expiration:
Token Storage:
Password Security:
CORS Configuration:
Token Configuration:
Password Storage: Always hash passwords before storing them in the DB. We use BCrypt in this implementation.
Token Security:
CORS Configuration: Properly configure CORS to only allow requests from trusted domains.
Error Handling: Never expose sensitive information in error messages.
Signup Process:
Login Process:
Protected Route Access:
Always test your authentication implementation thoroughly:
CORS Errors: Ensure proper CORS configuration in both frontend and backend.
Token Expiration: Implement proper token refresh mechanism or handle expiration gracefully.
Security Headers: Configure security headers (HSTS, CSP, etc.) for production.
JWT authentication provides a secure and scalable way to handle user authentication in modern web applications. While this implementation provides a solid foundation, consider adding features like:
Sign Up
Login
Succeed
DB Table