
This project demonstrates a complete, production-ready implementation of JWT (JSON Web Token)-based authentication and authorization using React JS for the frontend and Spring Boot 3 for the backend. It follows best practices for security, scalability, and maintainability while providing a role-based access control (RBAC) system.
Key Features
☑️ Backend (Spring Boot 3)
- JWT Authentication (Access Token + Refresh Token)
- Role-Based Authorization (Admin, User, etc.)
- Spring Security 6 with stateless session management
- Password Encryption using BCrypt
- Token Refresh Mechanism for extended sessions
- Custom Exception Handling with global error responses
- CORS Configuration for frontend-backend communication
- H2 Database (for development) with JPA/Hibernate
- API Documentation (Swagger/OpenAPI-ready)
☑️ Frontend (React JS)
- Protected Routes with role-based access
- Axios Interceptors for JWT token management
- Auth Context API for global state management
- Automatic Token Refresh on expiration
- Login & Registration Forms with validation
- Responsive UI with error handling
Technology Stack
Category | Technologies Used |
---|---|
Backend | Spring Boot 3, Spring Security 6, JWT, H2 Database, JPA/Hibernate, Lombok |
Frontend | React JS, React Router, Axios, Context API, Vite (or Create React App) |
Security | JWT Authentication, BCrypt Password Hashing, CSRF Protection, CORS |
Development | Maven (Backend), npm/yarn (Frontend), H2 Console (for DB access) |
Testing | JUnit, Mockito (Backend), Jest/React Testing Library (Frontend) |
Project Architecture
The system follows a modular and layered architecture:
- Backend (Spring Boot 3)
- REST API with JWT-based security
- Role-based endpoints (Admin-only, User-only, Public)
- Stateless authentication for scalability
- Database layer with JPA repositories
- Frontend (React JS)
- Dynamic routing with protected routes
- Token management in localStorage (or HTTP-only cookies)
- API service layer with Axios interceptors
- Global state for user authentication
Why This Implementation?
🔹 Secure: Uses industry-standard JWT with refresh tokens for session management.
🔹 Scalable: Stateless authentication allows horizontal scaling.
🔹 Modular: Clean separation of concerns (controllers, services, repositories).
🔹 Production-Ready: Includes error handling, logging, and environment configurations.
🔹 Extensible: Easy to add new roles, permissions, or third-party auth (OAuth2).
This project serves as a boilerplate for building secure, full-stack applications with React + Spring Boot, following best practices in authentication and authorization.
Backend Implementation (Spring Boot 3)
1. Security Configuration
// src/main/java/com/example/auth/config/SecurityConfig.java
package com.example.auth.config;
import com.example.auth.security.JwtAuthEntryPoint;
import com.example.auth.security.JwtAuthFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthEntryPoint authEntryPoint;
private final JwtAuthFilter jwtAuthFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(exception -> exception.authenticationEntryPoint(authEntryPoint))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
);
http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2. JWT Authentication Filter
// src/main/java/com/example/auth/security/JwtAuthFilter.java
package com.example.auth.security;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
userEmail = jwtUtils.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtUtils.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
3. JWT Utility Class
// src/main/java/com/example/auth/security/JwtUtils.java
package com.example.auth.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Service
public class JwtUtils {
@Value("${application.security.jwt.secret-key}")
private String secretKey;
@Value("${application.security.jwt.expiration}")
private long jwtExpiration;
@Value("${application.security.jwt.refresh-token.expiration}")
private long refreshExpiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public String generateToken(
Map<String, Object> extraClaims,
UserDetails userDetails
) {
return buildToken(extraClaims, userDetails, jwtExpiration);
}
public String generateRefreshToken(UserDetails userDetails) {
return buildToken(new HashMap<>(), userDetails, refreshExpiration);
}
private String buildToken(
Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration
) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
4. Authentication Controller
// src/main/java/com/example/auth/controller/AuthController.java
package com.example.auth.controller;
import com.example.auth.dto.AuthRequest;
import com.example.auth.dto.AuthResponse;
import com.example.auth.dto.RegisterRequest;
import com.example.auth.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@RequestBody RegisterRequest request) {
return ResponseEntity.ok(authService.register(request));
}
@PostMapping("/authenticate")
public ResponseEntity<AuthResponse> authenticate(@RequestBody AuthRequest request) {
return ResponseEntity.ok(authService.authenticate(request));
}
@PostMapping("/refresh-token")
public ResponseEntity<AuthResponse> refreshToken(
@RequestBody RefreshTokenRequest request
) {
return ResponseEntity.ok(authService.refreshToken(request));
}
}
5. Protected Controller with Role-Based Access
// src/main/java/com/example/auth/controller/AdminController.java
package com.example.auth.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public String adminAccess() {
return "Admin Board";
}
}
6. Entity Classes
// src/main/java/com/example/auth/model/User.java
package com.example.auth.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
private String firstName;
private String lastName;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
@OneToMany(mappedBy = "user")
private Set<Token> tokens;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
// src/main/java/com/example/auth/model/Role.java
package com.example.auth.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "roles")
public class Role implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Enumerated(EnumType.STRING)
@Column(unique = true)
private RoleName name;
@Override
public String getAuthority() {
return name.name();
}
public enum RoleName {
ROLE_USER,
ROLE_ADMIN
}
}
// src/main/java/com/example/auth/model/Token.java
package com.example.auth.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Token {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String token;
@Enumerated(EnumType.STRING)
private TokenType tokenType;
private boolean expired;
private boolean revoked;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
public enum TokenType {
BEARER,
REFRESH
}
}
7. Repository Interfaces
// src/main/java/com/example/auth/repository/UserRepository.java
package com.example.auth.repository;
import com.example.auth.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}
// src/main/java/com/example/auth/repository/RoleRepository.java
package com.example.auth.repository;
import com.example.auth.model.Role;
import com.example.auth.model.Role.RoleName;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface RoleRepository extends JpaRepository<Role, Integer> {
Optional<Role> findByName(RoleName name);
}
// src/main/java/com/example/auth/repository/TokenRepository.java
package com.example.auth.repository;
import com.example.auth.model.Token;
import com.example.auth.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
public interface TokenRepository extends JpaRepository<Token, Long> {
@Query("""
SELECT t FROM Token t
WHERE t.user.id = :userId
AND (t.expired = false OR t.revoked = false)
""")
List<Token> findAllValidTokensByUser(Long userId);
Optional<Token> findByToken(String token);
}
8. Service Layer Implementation
// src/main/java/com/example/auth/service/AuthServiceImpl.java
package com.example.auth.service;
import com.example.auth.dto.*;
import com.example.auth.exception.*;
import com.example.auth.model.*;
import com.example.auth.repository.*;
import com.example.auth.security.*;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashSet;
import java.util.Set;
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final TokenRepository tokenRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final JwtUtils jwtUtils;
private final UserDetailsServiceImpl userDetailsService;
@Override
@Transactional
public AuthResponse register(RegisterRequest request) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new EmailAlreadyExistsException("Email is already in use");
}
User user = User.builder()
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.firstName(request.getFirstName())
.lastName(request.getLastName())
.build();
Set<Role> roles = new HashSet<>();
Role userRole = roleRepository.findByName(Role.RoleName.ROLE_USER)
.orElseThrow(() -> new RoleNotFoundException("User role not found"));
roles.add(userRole);
user.setRoles(roles);
User savedUser = userRepository.save(user);
UserDetailsImpl userDetails = (UserDetailsImpl) userDetailsService.loadUserByUsername(savedUser.getEmail());
String accessToken = jwtUtils.generateToken(userDetails);
String refreshToken = jwtUtils.generateRefreshToken(userDetails);
saveUserToken(savedUser, accessToken);
return AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.email(user.getEmail())
.roles(user.getRoles().stream().map(Role::getName).toList())
.build();
}
@Override
public AuthResponse authenticate(AuthRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
String accessToken = jwtUtils.generateToken(userDetails);
String refreshToken = jwtUtils.generateRefreshToken(userDetails);
revokeAllUserTokens(userDetails.getId());
saveUserToken(userDetails.getUser(), accessToken);
return AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.email(userDetails.getEmail())
.roles(userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList())
.build();
}
@Override
public AuthResponse refreshToken(RefreshTokenRequest request) {
String refreshToken = request.getRefreshToken();
String userEmail = jwtUtils.extractUsername(refreshToken);
if (userEmail != null) {
UserDetailsImpl userDetails = (UserDetailsImpl) userDetailsService.loadUserByUsername(userEmail);
if (jwtUtils.isTokenValid(refreshToken, userDetails)) {
String accessToken = jwtUtils.generateToken(userDetails);
revokeAllUserTokens(userDetails.getId());
saveUserToken(userDetails.getUser(), accessToken);
return AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.email(userDetails.getEmail())
.roles(userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList())
.build();
}
}
throw new InvalidTokenException("Invalid refresh token");
}
private void saveUserToken(User user, String jwtToken) {
Token token = Token.builder()
.user(user)
.token(jwtToken)
.tokenType(Token.TokenType.BEARER)
.expired(false)
.revoked(false)
.build();
tokenRepository.save(token);
}
private void revokeAllUserTokens(Long userId) {
List<Token> validTokens = tokenRepository.findAllValidTokensByUser(userId);
if (validTokens.isEmpty()) return;
validTokens.forEach(token -> {
token.setExpired(true);
token.setRevoked(true);
});
tokenRepository.saveAll(validTokens);
}
}
Frontend Implementation (React JS)
1. Auth Context for State Management
// src/contexts/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { login as authLogin, register as authRegister, refreshToken as authRefreshToken } from '../api/auth';
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(localStorage.getItem('token'));
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
const initializeAuth = async () => {
try {
if (token) {
// Verify token and get user info
const userData = await verifyToken(token);
setUser(userData);
}
} catch (error) {
console.error('Authentication check failed:', error);
logout();
} finally {
setLoading(false);
}
};
initializeAuth();
}, [token]);
const login = async (credentials) => {
try {
const response = await authLogin(credentials);
localStorage.setItem('token', response.token);
setToken(response.token);
setUser(response.user);
navigate('/dashboard');
} catch (error) {
throw error;
}
};
const register = async (userData) => {
try {
const response = await authRegister(userData);
localStorage.setItem('token', response.token);
setToken(response.token);
setUser(response.user);
navigate('/dashboard');
} catch (error) {
throw error;
}
};
const logout = () => {
localStorage.removeItem('token');
setToken(null);
setUser(null);
navigate('/login');
};
const refreshAuthToken = async () => {
try {
const response = await authRefreshToken();
localStorage.setItem('token', response.token);
setToken(response.token);
return response.token;
} catch (error) {
logout();
throw error;
}
};
return (
<AuthContext.Provider value={{ user, token, login, register, logout, refreshAuthToken, loading }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
2. API Service for Authentication
// src/api/auth.js
import axios from 'axios';
const API_URL = 'http://localhost:8080/api/auth';
const axiosInstance = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Add request interceptor to include token
axiosInstance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Add response interceptor to 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 { refreshAuthToken } = useAuth();
const newToken = await refreshAuthToken();
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return axiosInstance(originalRequest);
} catch (refreshError) {
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export const login = async (credentials) => {
const response = await axiosInstance.post('/authenticate', credentials);
return response.data;
};
export const register = async (userData) => {
const response = await axiosInstance.post('/register', userData);
return response.data;
};
export const refreshToken = async () => {
const response = await axiosInstance.post('/refresh-token');
return response.data;
};
export const getProtectedData = async () => {
const response = await axiosInstance.get('/protected');
return response.data;
};
3. Protected Route Component
// src/components/ProtectedRoute.jsx
import { useAuth } from '../contexts/AuthContext';
import { Navigate, Outlet } from 'react-router-dom';
const ProtectedRoute = ({ roles }) => {
const { user, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
if (!user) {
return <Navigate to="/login" replace />;
}
if (roles && !roles.some(role => user.roles.includes(role))) {
return <Navigate to="/unauthorized" replace />;
}
return <Outlet />;
};
export default ProtectedRoute;
4. Login Page Example
// src/pages/Login.jsx
import { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { useNavigate } from 'react-router-dom';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
await login({ email, password });
navigate('/dashboard');
} catch (err) {
setError('Invalid credentials. Please try again.');
console.error('Login error:', err);
}
};
return (
<div className="login-container">
<h2>Login</h2>
{error && <div className="alert alert-danger">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Email</label>
<input
type="email"
className="form-control"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="form-group">
<label>Password</label>
<input
type="password"
className="form-control"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" className="btn btn-primary">
Login
</button>
</form>
</div>
);
};
export default Login;
5. App Router Setup with Protected Routes
// src/App.jsx
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import ProtectedRoute from './components/ProtectedRoute';
import Login from './pages/Login';
import Register from './pages/Register';
import Dashboard from './pages/Dashboard';
import AdminPanel from './pages/AdminPanel';
import Home from './pages/Home';
import Unauthorized from './pages/Unauthorized';
function App() {
return (
<Router>
<AuthProvider>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/unauthorized" element={<Unauthorized />} />
{/* Protected routes */}
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
</Route>
{/* Admin-only routes */}
<Route element={<ProtectedRoute roles={['ADMIN']} />}>
<Route path="/admin" element={<AdminPanel />} />
</Route>
</Routes>
</AuthProvider>
</Router>
);
}
export default App;
6. React Auth Service
// src/services/auth.service.js
import axios from 'axios';
const API_URL = 'http://localhost:8080/api/auth';
class AuthService {
login(email, password) {
return axios
.post(`${API_URL}/authenticate`, { email, password })
.then(response => {
if (response.data.accessToken) {
localStorage.setItem('user', JSON.stringify(response.data));
}
return response.data;
});
}
logout() {
localStorage.removeItem('user');
}
register(firstName, lastName, email, password) {
return axios.post(`${API_URL}/register`, {
firstName,
lastName,
email,
password
});
}
getCurrentUser() {
return JSON.parse(localStorage.getItem('user'));
}
refreshToken(refreshToken) {
return axios.post(`${API_URL}/refresh-token`, { refreshToken })
.then(response => {
if (response.data.accessToken) {
const user = JSON.parse(localStorage.getItem('user'));
user.accessToken = response.data.accessToken;
localStorage.setItem('user', JSON.stringify(user));
}
return response.data;
});
}
}
export default new AuthService();
7. Axios Interceptor Setup
// src/utils/axiosInterceptor.js
import axios from 'axios';
import authService from '../services/auth.service';
const api = axios.create({
baseURL: 'http://localhost:8080/api'
});
api.interceptors.request.use(
config => {
const user = authService.getCurrentUser();
if (user?.accessToken) {
config.headers.Authorization = `Bearer ${user.accessToken}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const user = authService.getCurrentUser();
if (user?.refreshToken) {
try {
const { accessToken } = await authService.refreshToken(user.refreshToken);
axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (refreshError) {
authService.logout();
window.location = '/login';
return Promise.reject(refreshError);
}
}
}
return Promise.reject(error);
}
);
export default api;
8. Complete React Components
Auth Guard Component
// src/components/AuthGuard.jsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import authService from '../services/auth.service';
const AuthGuard = ({ children, roles }) => {
const navigate = useNavigate();
const user = authService.getCurrentUser();
useEffect(() => {
if (!user) {
navigate('/login');
} else if (roles && !roles.some(role => user.roles.includes(role))) {
navigate('/unauthorized');
}
}, [user, navigate, roles]);
return user ? children : null;
};
export default AuthGuard;
Login Component
// src/pages/Login.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import AuthService from '../services/auth.service';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
try {
await AuthService.login(email, password);
navigate('/profile');
} catch (err) {
setError('Login failed. Please check your credentials.');
console.error(err);
}
};
return (
<div className="container mt-5">
<div className="row justify-content-center">
<div className="col-md-6">
<div className="card">
<div className="card-header">
<h4>Login</h4>
</div>
<div className="card-body">
{error && <div className="alert alert-danger">{error}</div>}
<form onSubmit={handleLogin}>
<div className="form-group mb-3">
<label>Email</label>
<input
type="email"
className="form-control"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="form-group mb-3">
<label>Password</label>
<input
type="password"
className="form-control"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" className="btn btn-primary w-100">
Login
</button>
</form>
</div>
</div>
</div>
</div>
</div>
);
};
export default Login;
Profile Component (Protected)
// src/pages/Profile.jsx
import { useEffect, useState } from 'react';
import AuthService from '../services/auth.service';
const Profile = () => {
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
const user = AuthService.getCurrentUser();
if (user) {
setCurrentUser(user);
}
}, []);
return (
<div className="container">
{currentUser && (
<div className="card mt-5">
<div className="card-header">
<h3>
<strong>{currentUser.email}</strong> Profile
</h3>
</div>
<div className="card-body">
<p>
<strong>Token:</strong> {currentUser.accessToken.substring(0, 20)} ...{' '}
{currentUser.accessToken.substr(currentUser.accessToken.length - 20)}
</p>
<p>
<strong>Roles:</strong> {currentUser.roles.join(', ')}
</p>
</div>
</div>
)}
</div>
);
};
export default Profile;
Key Features Implemented
- Backend:
- JWT authentication with access and refresh tokens
- Role-based authorization (USER, ADMIN)
- Secure password storage with BCrypt
- Stateless session management
- Token refresh mechanism
- Frontend:
- Auth context for global state management
- Protected routes with role checking
- Axios interceptors for automatic token refresh
- Login/registration forms with error handling
- Token storage in localStorage with security considerations
- Security Best Practices:
- HTTP-only cookies could be used instead of localStorage for better security
- Short-lived access tokens with refresh tokens
- CSRF protection on backend
- Proper error handling and logging
This implementation provides a solid foundation for a secure React + Spring Boot application with JWT authentication and role-based authorization.