Integrating JWT Authentication with Spring Boot and React

by Didin J. on May 19, 2025 Integrating JWT Authentication with Spring Boot and React

Build a secure full-stack app with Spring Boot and React using JWT authentication. Includes login, registration, roles, token refresh, and styling


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

Integrating JWT Authentication with Spring Boot and React - spring initializr

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.

Integrating JWT Authentication with Spring Boot and React - register

Integrating JWT Authentication with Spring Boot and React - register error

Integrating JWT Authentication with Spring Boot and React - login

Integrating JWT Authentication with Spring Boot and React - secure page


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:

Thanks!