Looking to implement secure user authentication in your full-stack application? In this step-by-step tutorial, you’ll learn how to integrate JWT authentication with Spring Boot and React. We'll build a backend API using Spring Boot 3 and Spring Security to issue and validate JWT tokens, as well as a React frontend that handles login, stores tokens, and accesses protected resources.
Whether you're building a modern web app or a RESTful API with frontend integration, this guide will show you how to set up JWT-based authentication correctly using best practices.
Prerequisites
- Java 17+
- Node.js + npm
- Spring Boot 3.x
- React 18+
- IDE (IntelliJ / VS Code)
- PostgreSQL
1. Spring Boot Backend Setup
1.1 Create a Spring Boot Project
Use Spring Initializr with these settings:
- Project: Maven
- Language: Java
- Dependencies:
- Spring Web
- Spring Security
- Spring Data JPA
- PostgreSQL
- Lombok
Download, extract to your projects folder, then open it with your favorite IDE.
1.2 Project Structure
src/main/java/com/example/jwtsecurity/
├── controller/
├── model/
├── repository/
├── security/
├── service/
└── JwtSecurityApplication.java
1.3 Create the PostgreSQL Database
Open your terminal and run the following commands to create a PostgreSQL database:
psql postgres -U djamware
Once inside the PostgreSQL shell, create the database:
CREATE DATABASE spring_jwt_auth;
\q
Replace spring_restapi with your preferred database name if different.
1.4 Configure Database
In src/main/resources/application.properties:
spring.application.name=spring-restapi
spring.datasource.url=jdbc:postgresql://localhost:5432/spring_jwt_auth
spring.datasource.username=postgres
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
2. Implement the Backend
2.1 User Entity
Create a new entities folder, then create a Java class src/main/java/com/djamware/spring_jwt_auth/entities/User.java.
mkdir src/main/java/com/djamware/spring_jwt_auth/entities
In src/main/java/com/djamware/spring_jwt_auth/entities/User.java, replace with:
package com.djamware.spring_jwt_auth.entities;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name="users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
}
2.2 UserRepository
Create a new repositories folder, then create a Java interface src/main/java/com/djamware/spring_jwt_auth/repositories/UserRepository.java.
mkdir src/main/java/com/djamware/spring_jwt_auth/repositories
In src/main/java/com/djamware/spring_jwt_auth/repositories/UserRepository.java, replace with:
package com.djamware.spring_jwt_auth.repositories;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.djamware.spring_jwt_auth.entities.User;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
2.3 UserDetailsServiceImpl
This class tells Spring Security how to load user details (typically from your database) during authentication. Create a new services folder, then create a Java class src/main/java/com/djamware/spring_jwt_auth/services/UserDetailsServiceImpl.java.
mkdir src/main/java/com/djamware/spring_jwt_auth/services
In src/main/java/com/djamware/spring_jwt_auth/services/UserDetailsServiceImpl.java, replace with:
package com.djamware.spring_jwt_auth.services;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.djamware.spring_jwt_auth.entities.User;
import com.djamware.spring_jwt_auth.repositories.UserRepository;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Autowired
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")));
}
}
2.4 JwtFilter
This filter will intercept incoming requests, extract the JWT token, validate it, and set the authentication in the security context. Create a Java class src/main/java/com/djamware/spring_jwt_auth/configs/JwtFilter.java.
In src/main/java/com/djamware/spring_jwt_auth/configs/JwtFilter.java, replace with:
package com.djamware.spring_jwt_auth.configs;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
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.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.djamware.spring_jwt_auth.services.UserDetailsServiceImpl;
import com.djamware.spring_jwt_auth.utils.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
@Autowired
public JwtFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
String username = null;
String token = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
username = jwtUtil.extractUsername(token);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
2.5 Add JJWT Dependency to pom.xml
Add this to your pom.xml dependencies:
<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>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
2.6 JWT Utility
In application.properties, add:
jwt.secret=your-base64-secret-here
Create a new utils folder, then create a Java class src/main/java/com/djamware/spring_jwt_auth/utils/JwtUtil.java.
mkdir src/main/java/com/djamware/spring_jwt_auth/utils
In src/main/java/com/djamware/spring_jwt_auth/utils/JwtUtil.java, replace with:
package com.djamware.spring_jwt_auth.utils;
import java.util.Base64;
import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
private SecretKey getSigningKey() {
byte[] decodedKey = Base64.getDecoder().decode(secret);
return Keys.hmacShaKeyFor(decodedKey);
}
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token, UserDetails userDetails) {
return extractUsername(token).equals(userDetails.getUsername());
}
}
2.7 Security Configuration
Create a new configs folder, then create a Java class src/main/java/com/djamware/spring_jwt_auth/utils/SecurityConfig.java.
mkdir src/main/java/com/djamware/spring_jwt_auth/configs
In src/main/java/com/djamware/spring_jwt_auth/configs/SecurityConfig.java, replace with:
package com.djamware.spring_jwt_auth.configs;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
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
public class SecurityConfig {
private final JwtFilter jwtFilter;
@Autowired
public SecurityConfig(JwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(authProvider);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.8 Auth DTOs
Create a new dtos folder, then create a Java class src/main/java/com/djamware/spring_jwt_auth/dtos/AuthRequest.java and AuthResponse.java.
mkdir src/main/java/com/djamware/spring_jwt_auth/dtos
In src/main/java/com/djamware/spring_jwt_auth/dtos/AuthRequest.java, replace with:
package com.djamware.spring_jwt_auth.dtos;
import lombok.Data;
@Data
public class AuthRequest {
private String username;
private String password;
}
In src/main/java/com/djamware/spring_jwt_auth/dtos/AuthResponse.java, replace with:
package com.djamware.spring_jwt_auth.dtos;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class AuthResponse {
private String token;
}
2.9 AuthService
Create a Java class src/main/java/com/djamware/spring_jwt_auth/services/AuthService.java. In src/main/java/com/djamware/spring_jwt_auth/services/AuthService.java, replace with:
package com.djamware.spring_jwt_auth.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import com.djamware.spring_jwt_auth.dtos.AuthRequest;
import com.djamware.spring_jwt_auth.dtos.AuthResponse;
import com.djamware.spring_jwt_auth.utils.JwtUtil;
@Service
public class AuthService {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
@Autowired
public AuthService(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
}
public AuthResponse authenticate(AuthRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtUtil.generateToken(userDetails);
return new AuthResponse(token);
}
}
2.10 Authentication Controller
Create a new controllers folder, then create a Java class src/main/java/com/djamware/spring_jwt_auth/controllers/AuthController.java.
mkdir src/main/java/com/djamware/spring_jwt_auth/controllers
In src/main/java/com/djamware/spring_jwt_auth/controllers/AuthController.java, replace with:
package com.djamware.spring_jwt_auth.controllers;
import org.springframework.beans.factory.annotation.Autowired;
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;
import com.djamware.spring_jwt_auth.dtos.AuthRequest;
import com.djamware.spring_jwt_auth.dtos.AuthResponse;
import com.djamware.spring_jwt_auth.services.AuthService;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthService authService;
@Autowired
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest loginRequest) {
AuthResponse response = authService.authenticate(loginRequest);
return ResponseEntity.ok(response);
}
}
2.11 Secure Hello Controller
Create a new Java class src/main/java/com/djamware/spring_jwt_auth/controllers/HelloController.java, then replace it with:
package com.djamware.spring_jwt_auth.controllers;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin(origins = "http://localhost:3000")
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello from the secured backend!";
}
}
2.12 Protect Secure Endpoint
In SecurityConfig.java, update:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/hello").authenticated()
.anyRequest().authenticated())
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
3. React Frontend Setup
3.1 Create React App
Go to your React projects folder, then:
npx create-react-app jwt-auth-client
cd jwt-auth-client
npm install axios react-router-dom
Open that React project with your favorite IDE.
3.2 Login Page
Create a new screens folder inside the src folder, then create a JavaScript file src/screens/Login.js.
mkdir src/screens
In src/screens/Login.js, replace with:
import React, { useState } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
export default function Login({ onLogin }) {
const [form, setForm] = useState({ username: '', password: '' });
const handleChange = (e) => setForm({ ...form, [e.target.name]: e.target.value });
const handleSubmit = async (e) => {
e.preventDefault();
try {
const res = await axios.post('http://localhost:8080/auth/login', form);
localStorage.setItem('token', res.data.token);
onLogin();
} catch {
alert('Login failed');
}
};
return (
<form onSubmit={handleSubmit}>
<input name="username" value={form.username} onChange={handleChange} placeholder="Username" />
<input name="password" type="password" value={form.password} onChange={handleChange} placeholder="Password" />
<button type="submit">Login</button>
</form>
);
}
Login.propTypes = {
onLogin: PropTypes.func.isRequired,
};
3.3 Axios Interceptor
Create a src/axiosConfig.js and replace it with:
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:8080/',
});
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
export default api;
3.4 App with Routes
In src/App.js, replace with:
import React, { useState } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Login from "./screens/Login";
import Home from "./screens/Home";
function App() {
const [authenticated, setAuthenticated] = useState(
!!localStorage.getItem("token")
);
return (
<BrowserRouter>
<Routes>
<Route
path="/login"
element={<Login onLogin={() => setAuthenticated(true)} />}
/>
<Route
path="/"
element={authenticated ? <Home /> : <Navigate to="/login" />}
/>
</Routes>
</BrowserRouter>
);
}
export default App;
3.5 Protected Home Component
Create src/screens/Home.js, then replace with:
import React, { useEffect, useState } from "react";
import api from "./axiosConfig";
export default function Home() {
const [message, setMessage] = useState("");
useEffect(() => {
api
.get("/hello")
.then(res => setMessage(res.data))
.catch(() => setMessage("Not authorized"));
}, []);
return (
<h2>
{message}
</h2>
);
}
4. Test the App
1. Run Spring Boot backend:
mvn spring-boot:run
2. Run React frontend:
npm start
3. Open the app in the browser and test the login with a test user from PostgreSQL.
5. Add Registration API with Roles
5.1 Update the User Entity
Add a roles field as a Set<Role>:
package com.djamware.spring_jwt_auth.entities;
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
}
5.2 Add a Role Entity
Create a src/main/java/com/djamware/spring_jwt_auth/entities/Role.java, then replace all with:
package com.djamware.spring_jwt_auth.entities;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "roles")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private ERole name;
}
5.3 Add a Role Entity
Create a src/main/java/com/djamware/spring_jwt_auth/entities/ERole.java, then replace all with:
package com.djamware.spring_jwt_auth.entities;
public enum ERole {
ROLE_USER,
ROLE_ADMIN
}
5.4 Add the Register DTOs
Create a src/main/java/com/djamware/spring_jwt_auth/dtos/RegisterRequest.java, then replace:
package com.djamware.spring_jwt_auth.dtos;
import java.util.Set;
import lombok.Data;
@Data
public class RegisterRequest {
private String username;
private String password;
private Set<String> roles;
}
Create a src/main/java/com/djamware/spring_jwt_auth/dtos/MessageResponse.java, then replace:
package com.djamware.spring_jwt_auth.dtos;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class MessageResponse {
private String message;
}
5.5 Add Register Repositories
Update src/main/java/com/djamware/spring_jwt_auth/repositories/UserRepository.java with:
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Boolean existsByUsername(String username);
}
Create a src/main/java/com/djamware/spring_jwt_auth/repositories/RoleRepository.java, then replace:
package com.djamware.spring_jwt_auth.repositories;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.djamware.spring_jwt_auth.entities.ERole;
import com.djamware.spring_jwt_auth.entities.Role;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(ERole name);
boolean existsByName(ERole name);
}
5.6 Add Register to AuthService
In src/main/java/com/djamware/spring_jwt_auth/services/AuthService.java, update with:
package com.djamware.spring_jwt_auth.services;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.djamware.spring_jwt_auth.dtos.AuthRequest;
import com.djamware.spring_jwt_auth.dtos.AuthResponse;
import com.djamware.spring_jwt_auth.dtos.MessageResponse;
import com.djamware.spring_jwt_auth.dtos.RegisterRequest;
import com.djamware.spring_jwt_auth.entities.ERole;
import com.djamware.spring_jwt_auth.entities.Role;
import com.djamware.spring_jwt_auth.entities.User;
import com.djamware.spring_jwt_auth.repositories.RoleRepository;
import com.djamware.spring_jwt_auth.repositories.UserRepository;
import com.djamware.spring_jwt_auth.utils.JwtUtil;
@Service
public class AuthService {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
@Autowired
public AuthService(AuthenticationManager authenticationManager, JwtUtil jwtUtil, UserRepository userRepository,
RoleRepository roleRepository,
PasswordEncoder passwordEncoder) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
}
public AuthResponse authenticate(AuthRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtUtil.generateToken(userDetails);
return new AuthResponse(token);
}
public MessageResponse register(RegisterRequest request) {
if (userRepository.existsByUsername(request.getUsername())) {
return new MessageResponse("Error: Username is already taken!");
}
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
Set<String> strRoles = request.getRoles();
Set<Role> roles = new HashSet<>();
if (strRoles == null || strRoles.isEmpty()) {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role not found."));
roles.add(userRole);
} else {
strRoles.forEach(role -> {
if ("admin".equalsIgnoreCase(role)) {
Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
.orElseThrow(() -> new RuntimeException("Error: Role not found."));
roles.add(adminRole);
} else {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role not found."));
roles.add(userRole);
}
});
}
user.setRoles(roles);
userRepository.save(user);
return new MessageResponse("User registered successfully!");
}
}
5.7 Add Register to AuthController
In src/main/java/com/djamware/spring_jwt_auth/controllers/AuthController.java, update with:
import com.djamware.spring_jwt_auth.dtos.AuthRequest;
import com.djamware.spring_jwt_auth.dtos.AuthResponse;
import com.djamware.spring_jwt_auth.dtos.MessageResponse;
import com.djamware.spring_jwt_auth.dtos.RegisterRequest;
import com.djamware.spring_jwt_auth.services.AuthService;
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
@Autowired
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest loginRequest) {
AuthResponse response = authService.authenticate(loginRequest);
return ResponseEntity.ok(response);
}
@PostMapping("/register")
public ResponseEntity<MessageResponse> registerUser(@RequestBody RegisterRequest request) {
MessageResponse response = authService.register(request);
if (response.getMessage().startsWith("Error")) {
return ResponseEntity.badRequest().body(response);
}
return ResponseEntity.ok(response);
}
}
5.8 Optional: Role Initialization
You can initialize roles during application startup, update SpringJwtAuthApplication.java:
package com.djamware.spring_jwt_auth;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.djamware.spring_jwt_auth.entities.ERole;
import com.djamware.spring_jwt_auth.entities.Role;
import com.djamware.spring_jwt_auth.repositories.RoleRepository;
@SpringBootApplication
public class SpringJwtAuthApplication {
public static void main(String[] args) {
SpringApplication.run(SpringJwtAuthApplication.class, args);
}
@Bean
CommandLineRunner initRoles(RoleRepository roleRepository) {
return args -> {
if (!roleRepository.existsByName(ERole.ROLE_USER)) {
roleRepository.save(new Role(null, ERole.ROLE_USER));
}
if (!roleRepository.existsByName(ERole.ROLE_ADMIN)) {
roleRepository.save(new Role(null, ERole.ROLE_ADMIN));
}
};
}
}
6. Registration Form Component (React + Axios)
6.1 Register Form
1. Create a src/screens/RegisterForm.js, add:
import React, { useState } from "react";
import axios from "axios";
import PropTypes from "prop-types";
const RegisterForm = ({ onRegisterSuccess }) => {
const [formData, setFormData] = useState({
username: "",
password: "",
roles: ["user"]
});
const [message, setMessage] = useState(null);
const [error, setError] = useState(null);
const handleChange = e => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async e => {
e.preventDefault();
setError(null);
setMessage(null);
try {
const response = await axios.post(
"http://localhost:8080/api/auth/register",
formData
);
setMessage(response.data.message);
onRegisterSuccess?.();
} catch (err) {
setError(err.response?.data?.message ?? 'Registration failed');
}
};
return (
<div className="p-4 rounded-lg shadow-md max-w-md mx-auto mt-8 bg-white">
<h2 className="text-xl font-bold mb-4">Register</h2>
{message &&
<p className="text-green-600">
{message}
</p>}
{error &&
<p className="text-red-600">
{error}
</p>}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium">Username</label>
<input
type="text"
name="username"
className="mt-1 p-2 border rounded w-full"
value={formData.username}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">Password</label>
<input
type="password"
name="password"
className="mt-1 p-2 border rounded w-full"
value={formData.password}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="roles" className="block text-sm font-medium">Role</label>
<select
name="roles"
className="mt-1 p-2 border rounded w-full"
value={formData.roles[0]}
onChange={e =>
setFormData(prev => ({ ...prev, roles: [e.target.value] }))}
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white p-2 rounded"
>
Register
</button>
</form>
</div>
);
};
RegisterForm.propTypes = {
onRegisterSuccess: PropTypes.func.isRequired
};
export default RegisterForm;
6.2 Usage Example
In src/App.js, update:
import React, { useState } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Login from "./screens/Login";
import Home from "./screens/Home";
import RegisterForm from "./RegisterForm";
function App() {
const [authenticated, setAuthenticated] = useState(
!!localStorage.getItem("token")
);
return (
<BrowserRouter>
<Routes>
<Route
path="/login"
element={<Login onLogin={() => setAuthenticated(true)} />}
/>
<Route
path="/"
element={authenticated ? <Home /> : <Navigate to="/login" />}
/>
<Route path="/register" element={<RegisterForm />} />
</Routes>
</BrowserRouter>
);
}
export default App;
6.3 CORS Configuration in Spring Boot (if needed)
To allow requests from the frontend (e.g., running on http://localhost:3000), add this to your backend:
Create a src/main/java/com/djamware/spring_jwt_auth/configs/WebConfig.java, then replace all:
package com.djamware.spring_jwt_auth.configs;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
In AuthController.java, add:
import org.springframework.web.bind.annotation.CrossOrigin;
@CrossOrigin(origins = "http://localhost:3000")
@RestController
@RequestMapping("/auth")
public class AuthController {
...
}
Or, in SecurityConfig.java, add:
import java.util.List;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/hello").authenticated()
.anyRequest().authenticated())
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
...
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
6.4 Add Logout
In Home.js, update:
import React, { useEffect, useState } from "react";
import api from "../axiosConfig";
import { useNavigate } from "react-router-dom";
export default function Home() {
const [message, setMessage] = useState("");
const navigate = useNavigate();
useEffect(() => {
api
.get("/hello")
.then(res => setMessage(res.data))
.catch(() => setMessage("Not authorized"));
}, []);
const handleLogout = () => {
localStorage.removeItem("token"); // Clear the token
navigate("/login"); // Redirect to login
};
return (
<div>
<h1>
{message || "Welcome!"}
</h1>
<button onClick={handleLogout}>Logout</button>
</div>
);
}
7. Global Styles (Optional)
Create or update a file like src/App.css and import it in src/App.js:
body {
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f4f6f8;
}
.container {
max-width: 400px;
margin: 100px auto;
padding: 30px;
background-color: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
h2 {
margin-bottom: 20px;
text-align: center;
}
form input {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border-radius: 6px;
border: 1px solid #ccc;
}
button {
width: 100%;
padding: 10px;
background-color: #1976d2;
border: none;
color: white;
font-weight: bold;
border-radius: 6px;
cursor: pointer;
}
button:hover {
background-color: #1565c0;
}
.link {
display: block;
margin-top: 10px;
text-align: center;
color: #1976d2;
text-decoration: none;
}
form select {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border-radius: 6px;
border: 1px solid #ccc;
background-color: #fff;
font-size: 1rem;
}
Then in App.js:
import './App.css';
Update src/screens/LoginForm.js:
return (
<div className="container">
<h2>Login</h2>
{error && <p className="text-red-600">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-4">
<input
id="username"
name="username"
placeholder="Username"
type="text"
value={formData.username}
onChange={handleChange}
required
/>
<input
id="password"
name="password"
placeholder="Password"
type="password"
value={formData.password}
onChange={handleChange}
required
/>
<button type="submit">Login</button>
</form>
<Link to="/register" className="link">Don't have an account? Register</Link>
</div>
);
Update src/screens/RegisterForm.js:
import React, { useState } from "react";
import axios from "axios";
import PropTypes from "prop-types";
import { Link } from 'react-router-dom';
const RegisterForm = ({ onRegisterSuccess }) => {
const [formData, setFormData] = useState({
username: "",
password: "",
roles: ["user"]
});
const [message, setMessage] = useState(null);
const [error, setError] = useState(null);
const handleChange = e => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async e => {
e.preventDefault();
setError(null);
setMessage(null);
try {
const response = await axios.post(
"http://localhost:8080/api/auth/register",
formData
);
setMessage(response.data.message);
onRegisterSuccess?.();
} catch (err) {
setError(err.response?.data?.message ?? 'Registration failed');
}
};
return (
<div className="container">
<h2>Register</h2>
{message &&
<p className="text-green-600">
{message}
</p>}
{error &&
<p className="text-red-600">
{error}
</p>}
<form onSubmit={handleSubmit}>
<input
type="text"
name="username"
placeholder="Username"
className="mt-1 p-2 border rounded w-full"
value={formData.username}
onChange={handleChange}
required
/>
<input
type="password"
name="password"
placeholder="Password"
className="mt-1 p-2 border rounded w-full"
value={formData.password}
onChange={handleChange}
required
/>
<select
name="roles"
value={formData.roles[0]}
onChange={e =>
setFormData(prev => ({ ...prev, roles: [e.target.value] }))}
required
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Register</button>
</form>
<Link to="/login" className="link">Already have an account? Login</Link>
</div>
);
};
RegisterForm.propTypes = {
onRegisterSuccess: PropTypes.func.isRequired
};
export default RegisterForm;
Update src/screens/Home.js:
return (
<div className="container">
<h2>Home</h2>
<p>
{message}
</p>
<button onClick={handleLogout}>Logout</button>
</div>
);
8. Add Validation & Error Messages
8.1 Updated RegisterForm.js with Validation & Error Messages
import React, { useState } from "react";
import PropTypes from "prop-types";
import { Link, useNavigate } from "react-router-dom";
const RegisterForm = ({ onRegisterSuccess }) => {
const [formData, setFormData] = useState({
username: "",
password: "",
roles: ["user"]
});
const navigate = useNavigate();
const [errors, setErrors] = useState({});
const [serverError, setServerError] = useState("");
const handleChange = e => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
setErrors(prev => ({ ...prev, [name]: "" })); // clear field error
setServerError("");
};
const validate = () => {
const newErrors = {};
if (!formData.username.trim()) newErrors.username = "Username is required";
if (!formData.password.trim()) newErrors.password = "Password is required";
else if (formData.password.length < 6)
newErrors.password = "Password must be at least 6 characters";
return newErrors;
};
const handleSubmit = async e => {
e.preventDefault();
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
try {
const response = await fetch("http://localhost:8080/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: formData.username,
password: formData.password,
roles: [formData.role]
})
});
if (!response.ok) {
const result = await response.json();
throw new Error(result.message || "Registration failed");
}
navigate("/login");
} catch (err) {
setServerError(err.message);
}
};
return (
<div className="container">
<h2>Register</h2>
{serverError &&
<div className="error">
{serverError}
</div>}
<form onSubmit={handleSubmit}>
<input
type="text"
name="username"
placeholder="Username"
className="mt-1 p-2 border rounded w-full"
value={formData.username}
onChange={handleChange}
required
/>
{errors.username &&
<div className="error">
{errors.username}
</div>}
<input
type="password"
name="password"
placeholder="Password"
className="mt-1 p-2 border rounded w-full"
value={formData.password}
onChange={handleChange}
required
/>
{errors.password &&
<div className="error">
{errors.password}
</div>}
<select
name="roles"
value={formData.roles[0]}
onChange={e =>
setFormData(prev => ({ ...prev, roles: [e.target.value] }))}
required
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Register</button>
</form>
<Link to="/login" className="link">
Already have an account? Login
</Link>
</div>
);
};
RegisterForm.propTypes = {
onRegisterSuccess: PropTypes.func.isRequired
};
export default RegisterForm;
8.2 CSS Enhancements in App.css
.error {
color: red;
font-size: 0.9rem;
margin-top: -10px;
margin-bottom: 10px;
}
8.3 Test
1. Start your Spring Boot backend (localhost:8080)
2. Run your React app (localhost:3000)
3. Fill out the registration form and check the database or backend logs.
8. Conclusion
In this tutorial, you’ve learned how to implement JWT authentication with Spring Boot and React, covering everything from building a secure REST API to connecting it with a modern frontend application. We extended the basic login flow to include:
- ✅ User registration with role-based access
- 🔐 JWT generation and validation
- 🔄 Token refresh mechanism
- 🧑🤝🧑 User info storage using Context or Redux
- 🎨 Styled React UI with validation and error handling
With this setup, you now have a solid foundation for building secure, scalable full-stack applications using Spring Boot 3, React 18, PostgreSQL, and JWT.
For future enhancements, consider:
- Using Spring Security OAuth2 for social login
- Adding account activation and email verification
- Building an admin dashboard to manage users and roles
You can find the full source code for Spring Boot Backend and React Frontend on our GitHub repository.
That just the basic. If you need more deep learning about Java and Spring Framework you can take the following cheap course:
- Java basics, Java in Use //Complete course for beginners
- Java Programming: Master Basic Java Concepts
- Master Java Web Services and REST API with Spring Boot
- JDBC Servlets and JSP - Java Web Development Fundamentals
- The Complete Java Web Development Course
- Spring MVC For Beginners: Build Java Web App in 25 Steps
- Practical RESTful Web Services with Java EE 8 (JAX-RS 2.1)
- Mastering React JS
- Master React Native Animations
- React: React Native Mobile Development: 3-in-1
- MERN Stack Front To Back: Full Stack React, Redux & Node. js
- Learning React Native Development
Thanks!