Skip to main content

Complete Authentication & Authorization Guide

Spring Boot + React with JWT, RBAC, SSO, MFA, and More​


Table of Contents​

  1. Overview
  2. JWT (JSON Web Tokens)
  3. HttpOnly Cookies
  4. Complete Authentication Flow
  5. Role-Based Access Control (RBAC)
  6. Single Sign-On (SSO)
  7. Multi-Factor Authentication (MFA)
  8. One-Time Password (OTP)
  9. Complete Code Examples
  10. Security Best Practices
  11. Advanced Topics
  12. Production Checklist

Overview​

Authentication vs Authorization​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚
β”‚ AUTHENTICATION: "Who are you?" β”‚
β”‚ β”œβ”€ Login with username/password β”‚
β”‚ β”œβ”€ Verify identity β”‚
β”‚ └─ Issue token β”‚
β”‚ β”‚
β”‚ AUTHORIZATION: "What can you do?" β”‚
β”‚ β”œβ”€ Check user roles β”‚
β”‚ β”œβ”€ Verify permissions β”‚
β”‚ └─ Allow/deny access β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
AspectAuthenticationAuthorization
PurposeVerify identityControl access
Question"Who are you?""What can you do?"
ProcessLogin, credentialsCheck permissions
ResultUser identifiedAccess granted/denied
ExampleUsername/passwordAdmin can delete users
HappensOnce (at login)Every request

JWT (JSON Web Tokens)​

JWT Structure​

1. Header​

{
"alg": "HS256",
"typ": "JWT"
}

2. Payload (Claims)​

{
"sub": "user123",
"name": "John Doe",
"email": "john@example.com",
"role": "admin",
"iat": 1640000000,
"exp": 1640003600
}

3. Signature​

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)

Access Token vs Refresh Token​

AspectAccess TokenRefresh Token
PurposeAccess protected resourcesGet new access token
LifespanShort (15 min - 1 hour)Long (7-30 days)
StorageMemory/stateHttpOnly cookie
Sent withEvery API requestOnly to /refresh endpoint
PayloadUser info, roles, permissionsUser ID, token ID
RevocableNo (stateless)Yes (stored in DB)

Token Storage Options​

StorageSecurityXSS VulnerableCSRF VulnerableBest For
LocalStorage❌ Lowβœ… Yes❌ NoNever (avoid)
SessionStorage❌ Lowβœ… Yes❌ NoNever (avoid)
Memory (state)βœ… Good⚠️ Lost on refresh❌ NoAccess tokens
HttpOnly Cookieβœ… Best❌ Noβœ… Yes (use CSRF token)Refresh tokens

Recommended Approach:

Access Token  β†’ In-memory (React state)
Refresh Token β†’ HttpOnly cookie

HttpOnly Cookies​

Security Benefits​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Cookie Security Flags β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ HttpOnly: Prevents JavaScript access β”‚
β”‚ Secure: Only sent over HTTPS β”‚
β”‚ SameSite: Prevents CSRF attacks β”‚
β”‚ Domain: Limits cookie scope β”‚
β”‚ Path: Restricts cookie path β”‚
β”‚ Max-Age: Sets expiration β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Complete Authentication Flow​

Login Flow​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Login Flow Diagram β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

User React App Spring Boot Database
β”‚ β”‚ β”‚ β”‚
β”‚ 1. Enter credentials β”‚ β”‚ β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚ β”‚ β”‚
β”‚ β”‚ 2. POST /api/auth/login β”‚
β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚ β”‚
β”‚ β”‚ {email, password} β”‚ β”‚
β”‚ β”‚ β”‚ 3. Find user β”‚
β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚
β”‚ β”‚ β”‚<────────────────────
β”‚ β”‚ β”‚ 4. User data β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ 5. Verify passwordβ”‚
β”‚ β”‚ β”‚ (BCrypt) β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ 6. Generate tokensβ”‚
β”‚ β”‚ β”‚ - Access token β”‚
β”‚ β”‚ β”‚ - Refresh token β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ 7. Store refresh β”‚
β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 8. Response β”‚ β”‚
β”‚ β”‚<────────────────────── β”‚
β”‚ β”‚ { β”‚ β”‚
β”‚ β”‚ accessToken, β”‚ β”‚
β”‚ β”‚ user: {...} β”‚ β”‚
β”‚ β”‚ } β”‚ β”‚
β”‚ β”‚ + Set-Cookie: β”‚ β”‚
β”‚ β”‚ refreshToken β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ 9. Login success β”‚ 10. Store access β”‚ β”‚
β”‚<───────────────────────── token in state β”‚ β”‚
β”‚ β”‚ 11. Redirect to β”‚ β”‚
β”‚ β”‚ dashboard β”‚ β”‚

Role-Based Access Control (RBAC)​

RBAC Hierarchy​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RBAC Hierarchy β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

User
β”‚
has β”‚
↓
Role ──────────────────┐
β”‚ β”‚
has β”‚ has β”‚
↓ ↓
Permissions Inherited
from other roles

Example:

Admin Role
β”œβ”€β”€ user:create
β”œβ”€β”€ user:read
β”œβ”€β”€ user:update
β”œβ”€β”€ user:delete
β”œβ”€β”€ post:create
β”œβ”€β”€ post:read
β”œβ”€β”€ post:update
└── post:delete

Editor Role
β”œβ”€β”€ post:create
β”œβ”€β”€ post:read
β”œβ”€β”€ post:update
└── user:read (own)

Viewer Role
β”œβ”€β”€ post:read
└── user:read (own)

Complete Code Examples​

Spring Boot Backend Implementation​

1. Project Dependencies (pom.xml)​

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>

<groupId>com.example</groupId>
<artifactId>auth-service</artifactId>
<version>1.0.0</version>

<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Spring Boot Starter Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>

<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!-- Google Authenticator (for TOTP/MFA) -->
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.5.0</version>
</dependency>

<!-- OAuth2 Client (for SSO) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
</dependencies>
</project>

2. Application Properties​

# application.properties

# Server Configuration
server.port=8080
server.servlet.context-path=/api

# Database Configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/authdb
spring.datasource.username=postgres
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# JWT Configuration
jwt.secret=your-256-bit-secret-key-change-this-in-production
jwt.access-token-expiration=900000
jwt.refresh-token-expiration=604800000

# Cookie Configuration
cookie.secure=true
cookie.http-only=true
cookie.same-site=Strict
cookie.max-age=604800

# OAuth2 Configuration (Google)
spring.security.oauth2.client.registration.google.client-id=your-google-client-id
spring.security.oauth2.client.registration.google.client-secret=your-google-client-secret
spring.security.oauth2.client.registration.google.scope=profile,email

# OAuth2 Configuration (GitHub)
spring.security.oauth2.client.registration.github.client-id=your-github-client-id
spring.security.oauth2.client.registration.github.client-secret=your-github-client-secret
spring.security.oauth2.client.registration.github.scope=read:user,user:email

# CORS Configuration
cors.allowed-origins=http://localhost:3000,http://localhost:5173
cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
cors.allowed-headers=*
cors.allow-credentials=true

3. Entity Models​

// User.java
package com.example.auth.entity;

import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;

@Column(unique = true, nullable = false)
private String email;

@Column(nullable = false)
private String password;

private String name;

@Column(name = "phone_number")
private String phoneNumber;

@Column(name = "is_email_verified")
private boolean isEmailVerified = false;

@Column(name = "is_mfa_enabled")
private boolean isMfaEnabled = false;

@Column(name = "mfa_secret")
private String mfaSecret;

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();

@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();

@Column(name = "updated_at")
private LocalDateTime updatedAt = LocalDateTime.now();

@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
}

// Role.java
package com.example.auth.entity;

import jakarta.persistence.*;
import lombok.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "roles")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Role {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;

@Column(unique = true, nullable = false)
@Enumerated(EnumType.STRING)
private RoleType name;

private String description;

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<Permission> permissions = new HashSet<>();
}

// RoleType.java
package com.example.auth.entity;

public enum RoleType {
ADMIN,
EDITOR,
VIEWER,
USER
}

// Permission.java
package com.example.auth.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "permissions")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Permission {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;

@Column(unique = true, nullable = false)
private String name;

private String resource;

private String action;

private String description;
}

// RefreshToken.java
package com.example.auth.entity;

import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "refresh_tokens")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RefreshToken {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;

@Column(nullable = false, unique = true)
private String token;

@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;

@Column(name = "expires_at", nullable = false)
private LocalDateTime expiresAt;

@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();

public boolean isExpired() {
return LocalDateTime.now().isAfter(expiresAt);
}
}

// OtpToken.java
package com.example.auth.entity;

import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "otp_tokens")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OtpToken {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;

@Column(nullable = false)
private String otp;

@Column(nullable = false)
private String email;

@Enumerated(EnumType.STRING)
private OtpType type;

@Column(name = "expires_at")
private LocalDateTime expiresAt;

@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();

private boolean used = false;

public boolean isExpired() {
return LocalDateTime.now().isAfter(expiresAt);
}
}

// OtpType.java
package com.example.auth.entity;

public enum OtpType {
EMAIL_VERIFICATION,
PASSWORD_RESET,
MFA_LOGIN,
PHONE_VERIFICATION
}

4. Repository Interfaces​

// UserRepository.java
package com.example.auth.repository;

import com.example.auth.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, String> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}

// RoleRepository.java
package com.example.auth.repository;

import com.example.auth.entity.Role;
import com.example.auth.entity.RoleType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface RoleRepository extends JpaRepository<Role, String> {
Optional<Role> findByName(RoleType name);
}

// PermissionRepository.java
package com.example.auth.repository;

import com.example.auth.entity.Permission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface PermissionRepository extends JpaRepository<Permission, String> {
Optional<Permission> findByName(String name);
}

// RefreshTokenRepository.java
package com.example.auth.repository;

import com.example.auth.entity.RefreshToken;
import com.example.auth.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {
Optional<RefreshToken> findByToken(String token);
void deleteByUser(User user);
void deleteByToken(String token);
}

// OtpTokenRepository.java
package com.example.auth.repository;

import com.example.auth.entity.OtpToken;
import com.example.auth.entity.OtpType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface OtpTokenRepository extends JpaRepository<OtpToken, String> {
Optional<OtpToken> findByEmailAndOtpAndType(String email, String otp, OtpType type);
void deleteByEmail(String email);
}

5. JWT Utility​

// JwtUtil.java
package com.example.auth.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtUtil {

@Value("${jwt.secret}")
private String secret;

@Value("${jwt.access-token-expiration}")
private Long accessTokenExpiration;

@Value("${jwt.refresh-token-expiration}")
private Long refreshTokenExpiration;

private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}

// Generate Access Token
public String generateAccessToken(UserDetails userDetails, Map<String, Object> claims) {
return createToken(claims, userDetails.getUsername(), accessTokenExpiration);
}

// Generate Refresh Token
public String generateRefreshToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username, refreshTokenExpiration);
}

// Create Token
private String createToken(Map<String, Object> claims, String subject, Long expiration) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);

return Jwts.builder()
.claims(claims)
.subject(subject)
.issuedAt(now)
.expiration(expiryDate)
.signWith(getSigningKey())
.compact();
}

// Extract Username
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

// Extract Expiration
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

// Extract Claim
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

// Extract All Claims
private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}

// Check if Token is Expired
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

// Validate Token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}

6. Security Configuration​

// SecurityConfig.java
package com.example.auth.config;

import com.example.auth.security.JwtAuthenticationEntryPoint;
import com.example.auth.security.JwtAuthenticationFilter;
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.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;
private final JwtAuthenticationEntryPoint authenticationEntryPoint;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/auth/login",
"/auth/register",
"/auth/refresh",
"/auth/forgot-password",
"/auth/reset-password",
"/auth/verify-email",
"/oauth2/**"
).permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(ex ->
ex.authenticationEntryPoint(authenticationEntryPoint)
);

return http.build();
}

@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}

@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(
"http://localhost:3000",
"http://localhost:5173"
));
configuration.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "OPTIONS"
));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

7. JWT Authentication Filter​

// JwtAuthenticationFilter.java
package com.example.auth.security;

import com.example.auth.util.JwtUtil;
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 JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;
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");

if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

try {
final String jwt = authHeader.substring(7);
final String username = jwtUtil.extractUsername(jwt);

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);

authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);

SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}

filterChain.doFilter(request, response);
}
}

// JwtAuthenticationEntryPoint.java
package com.example.auth.security;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException, ServletException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"" +
authException.getMessage() + "\"}");
}
}

// CustomUserDetailsService.java
package com.example.auth.security;

import com.example.auth.entity.User;
import com.example.auth.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
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 java.util.Collection;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));

return org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.authorities(getAuthorities(user))
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(!user.isEmailVerified())
.build();
}

private Collection<? extends GrantedAuthority> getAuthorities(User user) {
return user.getRoles().stream()
.flatMap(role -> {
var roleAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());
var permissions = role.getPermissions().stream()
.map(permission -> new SimpleGrantedAuthority(permission.getName()))
.collect(Collectors.toList());
permissions.add(roleAuthority);
return permissions.stream();
})
.collect(Collectors.toSet());
}
}

8. DTOs (Data Transfer Objects)​

// LoginRequest.java
package com.example.auth.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

@Data
public class LoginRequest {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;

@NotBlank(message = "Password is required")
private String password;

private String mfaCode;
}

// RegisterRequest.java
package com.example.auth.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class RegisterRequest {
@NotBlank(message = "Name is required")
private String name;

@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;

@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;

private String phoneNumber;
}

// AuthResponse.java
package com.example.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {
private String accessToken;
private String tokenType = "Bearer";
private UserDto user;
private boolean mfaRequired;
}

// UserDto.java
package com.example.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Set;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private String id;
private String email;
private String name;
private String phoneNumber;
private boolean emailVerified;
private boolean mfaEnabled;
private Set<String> roles;
private Set<String> permissions;
}

// RefreshTokenRequest.java
package com.example.auth.dto;

import lombok.Data;

@Data
public class RefreshTokenRequest {
private String refreshToken;
}

// MessageResponse.java
package com.example.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class MessageResponse {
private String message;
}

// ErrorResponse.java
package com.example.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse {
private String error;
private String message;
private int status;
private LocalDateTime timestamp;
}

9. Authentication Service​

// AuthService.java
package com.example.auth.service;

import com.example.auth.dto.*;
import com.example.auth.entity.*;
import com.example.auth.repository.*;
import com.example.auth.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class AuthService {

private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final OtpTokenRepository otpTokenRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
private final AuthenticationManager authenticationManager;
private final EmailService emailService;
private final MfaService mfaService;

@Value("${jwt.refresh-token-expiration}")
private Long refreshTokenExpiration;

@Value("${cookie.max-age}")
private int cookieMaxAge;

// Register new user
@Transactional
public AuthResponse register(RegisterRequest request) {
// Check if user already exists
if (userRepository.existsByEmail(request.getEmail())) {
throw new RuntimeException("Email already registered");
}

// Create new user
User user = User.builder()
.email(request.getEmail())
.name(request.getName())
.password(passwordEncoder.encode(request.getPassword()))
.phoneNumber(request.getPhoneNumber())
.isEmailVerified(false)
.isMfaEnabled(false)
.build();

// Assign default role
Role userRole = roleRepository.findByName(RoleType.USER)
.orElseThrow(() -> new RuntimeException("Default role not found"));
user.getRoles().add(userRole);

userRepository.save(user);

// Send verification email
String otp = generateOtp();
saveOtp(user.getEmail(), otp, OtpType.EMAIL_VERIFICATION);
emailService.sendVerificationEmail(user.getEmail(), otp);

return AuthResponse.builder()
.user(mapToUserDto(user))
.build();
}

// Login
@Transactional
public AuthResponse login(LoginRequest request, HttpServletResponse response) {
// Authenticate user
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);

User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));

// Check MFA
if (user.isMfaEnabled()) {
if (request.getMfaCode() == null) {
return AuthResponse.builder()
.mfaRequired(true)
.build();
}

boolean isValid = mfaService.verifyTotp(user.getMfaSecret(), request.getMfaCode());
if (!isValid) {
throw new RuntimeException("Invalid MFA code");
}
}

// Generate tokens
Map<String, Object> claims = buildClaims(user);
String accessToken = jwtUtil.generateAccessToken(
(org.springframework.security.core.userdetails.UserDetails) authentication.getPrincipal(),
claims
);
String refreshToken = jwtUtil.generateRefreshToken(user.getEmail());

// Save refresh token
saveRefreshToken(user, refreshToken);

// Set refresh token as HttpOnly cookie
setRefreshTokenCookie(response, refreshToken);

return AuthResponse.builder()
.accessToken(accessToken)
.user(mapToUserDto(user))
.mfaRequired(false)
.build();
}

// Refresh access token
@Transactional
public AuthResponse refreshAccessToken(String refreshToken) {
// Verify refresh token
String username = jwtUtil.extractUsername(refreshToken);

User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));

// Check if refresh token exists in database
RefreshToken storedToken = refreshTokenRepository.findByToken(refreshToken)
.orElseThrow(() -> new RuntimeException("Refresh token not found"));

if (storedToken.isExpired()) {
refreshTokenRepository.delete(storedToken);
throw new RuntimeException("Refresh token expired");
}

// Generate new access token
Map<String, Object> claims = buildClaims(user);
String accessToken = jwtUtil.generateAccessToken(
org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.authorities(Collections.emptyList())
.build(),
claims
);

return AuthResponse.builder()
.accessToken(accessToken)
.user(mapToUserDto(user))
.build();
}

// Logout
@Transactional
public void logout(String refreshToken, HttpServletResponse response) {
if (refreshToken != null) {
refreshTokenRepository.deleteByToken(refreshToken);
}

// Clear cookie
Cookie cookie = new Cookie("refreshToken", null);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}

// Verify email
@Transactional
public MessageResponse verifyEmail(String email, String otp) {
OtpToken otpToken = otpTokenRepository
.findByEmailAndOtpAndType(email, otp, OtpType.EMAIL_VERIFICATION)
.orElseThrow(() -> new RuntimeException("Invalid OTP"));

if (otpToken.isExpired() || otpToken.isUsed()) {
throw new RuntimeException("OTP expired or already used");
}

User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));

user.setEmailVerified(true);
userRepository.save(user);

otpToken.setUsed(true);
otpTokenRepository.save(otpToken);

return new MessageResponse("Email verified successfully");
}

// Forgot password
@Transactional
public MessageResponse forgotPassword(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));

String otp = generateOtp();
saveOtp(email, otp, OtpType.PASSWORD_RESET);
emailService.sendPasswordResetEmail(email, otp);

return new MessageResponse("Password reset OTP sent to email");
}

// Reset password
@Transactional
public MessageResponse resetPassword(String email, String otp, String newPassword) {
OtpToken otpToken = otpTokenRepository
.findByEmailAndOtpAndType(email, otp, OtpType.PASSWORD_RESET)
.orElseThrow(() -> new RuntimeException("Invalid OTP"));

if (otpToken.isExpired() || otpToken.isUsed()) {
throw new RuntimeException("OTP expired or already used");
}

User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));

user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);

otpToken.setUsed(true);
otpTokenRepository.save(otpToken);

// Invalidate all refresh tokens
refreshTokenRepository.deleteByUser(user);

return new MessageResponse("Password reset successful");
}

// Helper methods
private Map<String, Object> buildClaims(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("name", user.getName());
claims.put("roles", user.getRoles().stream()
.map(role -> role.getName().toString())
.collect(Collectors.toList()));
claims.put("permissions", user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.map(Permission::getName)
.collect(Collectors.toSet()));
return claims;
}

private UserDto mapToUserDto(User user) {
return UserDto.builder()
.id(user.getId())
.email(user.getEmail())
.name(user.getName())
.phoneNumber(user.getPhoneNumber())
.emailVerified(user.isEmailVerified())
.mfaEnabled(user.isMfaEnabled())
.roles(user.getRoles().stream()
.map(role -> role.getName().toString())
.collect(Collectors.toSet()))
.permissions(user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.map(Permission::getName)
.collect(Collectors.toSet()))
.build();
}

private void saveRefreshToken(User user, String token) {
RefreshToken refreshToken = RefreshToken.builder()
.token(token)
.user(user)
.expiresAt(LocalDateTime.now().plusSeconds(refreshTokenExpiration / 1000))
.build();
refreshTokenRepository.save(refreshToken);
}

private void setRefreshTokenCookie(HttpServletResponse response, String token) {
Cookie cookie = new Cookie("refreshToken", token);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(cookieMaxAge);
response.addCookie(cookie);
}

private String generateOtp() {
Random random = new Random();
return String.format("%06d", random.nextInt(999999));
}

private void saveOtp(String email, String otp, OtpType type) {
otpTokenRepository.deleteByEmail(email);

OtpToken otpToken = OtpToken.builder()
.email(email)
.otp(otp)
.type(type)
.expiresAt(LocalDateTime.now().plusMinutes(10))
.build();
otpTokenRepository.save(otpToken);
}
}

10. MFA Service​

// MfaService.java
package com.example.auth.service;

import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MfaService {

private final GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();

// Generate MFA secret
public String generateSecret() {
GoogleAuthenticatorKey key = googleAuthenticator.createCredentials();
return key.getKey();
}

// Generate QR code URL
public String generateQrCodeUrl(String email, String secret) {
return GoogleAuthenticatorQRGenerator.getOtpAuthURL(
"YourAppName",
email,
new GoogleAuthenticatorKey.Builder(secret).build()
);
}

// Verify TOTP code
public boolean verifyTotp(String secret, String code) {
try {
int codeInt = Integer.parseInt(code);
return googleAuthenticator.authorize(secret, codeInt);
} catch (NumberFormatException e) {
return false;
}
}
}

11. Email Service​

// EmailService.java
package com.example.auth.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class EmailService {

private final JavaMailSender mailSender;

public void sendVerificationEmail(String to, String otp) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject("Email Verification");
message.setText("Your verification code is: " + otp +
"\n\nThis code will expire in 10 minutes.");

try {
mailSender.send(message);
log.info("Verification email sent to: {}", to);
} catch (Exception e) {
log.error("Failed to send verification email: {}", e.getMessage());
}
}

public void sendPasswordResetEmail(String to, String otp) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject("Password Reset");
message.setText("Your password reset code is: " + otp +
"\n\nThis code will expire in 10 minutes.");

try {
mailSender.send(message);
log.info("Password reset email sent to: {}", to);
} catch (Exception e) {
log.error("Failed to send password reset email: {}", e.getMessage());
}
}
}

12. Authentication Controller​

// AuthController.java
package com.example.auth.controller;

import com.example.auth.dto.*;
import com.example.auth.service.AuthService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
return ResponseEntity.ok(authService.register(request));
}

@PostMapping("/login")
public ResponseEntity<AuthResponse> login(
@Valid @RequestBody LoginRequest request,
HttpServletResponse response) {
return ResponseEntity.ok(authService.login(request, response));
}

@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refresh(HttpServletRequest request) {
String refreshToken = getRefreshTokenFromCookie(request);
if (refreshToken == null) {
return ResponseEntity.status(401).build();
}
return ResponseEntity.ok(authService.refreshAccessToken(refreshToken));
}

@PostMapping("/logout")
public ResponseEntity<MessageResponse> logout(
HttpServletRequest request,
HttpServletResponse response) {
String refreshToken = getRefreshTokenFromCookie(request);
authService.logout(refreshToken, response);
return ResponseEntity.ok(new MessageResponse("Logged out successfully"));
}

@PostMapping("/verify-email")
public ResponseEntity<MessageResponse> verifyEmail(
@RequestParam String email,
@RequestParam String otp) {
return ResponseEntity.ok(authService.verifyEmail(email, otp));
}

@PostMapping("/forgot-password")
public ResponseEntity<MessageResponse> forgotPassword(@RequestParam String email) {
return ResponseEntity.ok(authService.forgotPassword(email));
}

@PostMapping("/reset-password")
public ResponseEntity<MessageResponse> resetPassword(
@RequestParam String email,
@RequestParam String otp,
@RequestParam String newPassword) {
return ResponseEntity.ok(authService.resetPassword(email, otp, newPassword));
}

private String getRefreshTokenFromCookie(HttpServletRequest request) {
if (request.getCookies() == null) {
return null;
}
return Arrays.stream(request.getCookies())
.filter(cookie -> "refreshToken".equals(cookie.getName()))
.findFirst()
.map(Cookie::getValue)
.orElse(null);
}
}

React Frontend Implementation​

1. Project Setup​

# Create React app with Vite
npm create vite@latest auth-frontend -- --template react
cd auth-frontend

# Install dependencies
npm install axios react-router-dom @tanstack/react-query
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

2. API Configuration​

// src/api/axios.js
import axios from 'axios';

const api = axios.create({
baseURL: 'http://localhost:8080/api',
withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
});

// Store access token in memory
let accessToken = null;

export const setAccessToken = (token) => {
accessToken = token;
};

export const getAccessToken = () => accessToken;

// Request interceptor
api.interceptors.request.use(
(config) => {
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);

// Response interceptor
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;

if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;

try {
const response = await axios.post(
'http://localhost:8080/api/auth/refresh',
{},
{ withCredentials: true }
);

const { accessToken: newToken } = response.data;
setAccessToken(newToken);

originalRequest.headers.Authorization = `Bearer ${newToken}`;
return api(originalRequest);
} catch (refreshError) {
setAccessToken(null);
window.location.href = '/login';
return Promise.reject(refreshError);
}
}

return Promise.reject(error);
}
);

export default api;

3. Authentication Context​

// src/context/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import api, { setAccessToken, getAccessToken } from '../api/axios';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
checkAuth();
}, []);

const checkAuth = async () => {
try {
const response = await api.post('/auth/refresh');
setAccessToken(response.data.accessToken);
setUser(response.data.user);
} catch (error) {
console.error('Not authenticated');
} finally {
setLoading(false);
}
};

const login = async (email, password, mfaCode = null) => {
const response = await api.post('/auth/login', {
email,
password,
mfaCode,
});

if (response.data.mfaRequired) {
return { mfaRequired: true };
}

setAccessToken(response.data.accessToken);
setUser(response.data.user);
return { success: true };
};

const register = async (data) => {
const response = await api.post('/auth/register', data);
return response.data;
};

const logout = async () => {
try {
await api.post('/auth/logout');
} catch (error) {
console.error('Logout error:', error);
} finally {
setAccessToken(null);
setUser(null);
window.location.href = '/login';
}
};

const verifyEmail = async (email, otp) => {
const response = await api.post('/auth/verify-email', null, {
params: { email, otp },
});
return response.data;
};

const forgotPassword = async (email) => {
const response = await api.post('/auth/forgot-password', null, {
params: { email },
});
return response.data;
};

const resetPassword = async (email, otp, newPassword) => {
const response = await api.post('/auth/reset-password', null, {
params: { email, otp, newPassword },
});
return response.data;
};

const value = {
user,
loading,
login,
register,
logout,
verifyEmail,
forgotPassword,
resetPassword,
hasRole: (role) => user?.roles?.includes(role),
hasPermission: (permission) => user?.permissions?.includes(permission),
};

return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}

export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};

4. Protected Route Component​

// src/components/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

export function ProtectedRoute({ children, roles = [], permissions = [] }) {
const { user, hasRole, hasPermission } = useAuth();

if (!user) {
return <Navigate to="/login" replace />;
}

if (roles.length > 0 && !roles.some((role) => hasRole(role))) {
return <Navigate to="/unauthorized" replace />;
}

if (
permissions.length > 0 &&
!permissions.some((perm) => hasPermission(perm))
) {
return <Navigate to="/unauthorized" replace />;
}

return children;
}

5. Login Component​

// src/pages/Login.jsx
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

export function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [mfaCode, setMfaCode] = useState('');
const [showMfa, setShowMfa] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);

const { login } = useAuth();
const navigate = useNavigate();

const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);

try {
const result = await login(email, password, showMfa ? mfaCode : null);

if (result.mfaRequired) {
setShowMfa(true);
setLoading(false);
return;
}

if (result.success) {
navigate('/dashboard');
}
} catch (err) {
setError(err.response?.data?.message || 'Login failed');
} finally {
setLoading(false);
}
};

return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
<div>
<h2 className="text-3xl font-bold text-center">Sign in</h2>
</div>

{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded">
{error}
</div>
)}

<form onSubmit={handleSubmit} className="space-y-6">
{!showMfa ? (
<>
<div>
<label className="block text-sm font-medium text-gray-700">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>

<div>
<label className="block text-sm font-medium text-gray-700">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
</>
) : (
<div>
<label className="block text-sm font-medium text-gray-700">
MFA Code
</label>
<input
type="text"
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value)}
required
maxLength={6}
placeholder="Enter 6-digit code"
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
)}

<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Signing in...' : showMfa ? 'Verify MFA' : 'Sign in'}
</button>
</form>

<div className="text-center space-y-2">
<Link to="/forgot-password" className="text-sm text-blue-600 hover:underline">
Forgot password?
</Link>
<div>
<span className="text-sm text-gray-600">Don't have an account? </span>
<Link to="/register" className="text-sm text-blue-600 hover:underline">
Sign up
</Link>
</div>
</div>
</div>
</div>
);
}

6. Register Component​

// src/pages/Register.jsx
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

export function Register() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
phoneNumber: '',
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);

const { register } = useAuth();
const navigate = useNavigate();

const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};

const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);

if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
setLoading(false);
return;
}

if (formData.password.length < 8) {
setError('Password must be at least 8 characters');
setLoading(false);
return;
}

try {
await register({
name: formData.name,
email: formData.email,
password: formData.password,
phoneNumber: formData.phoneNumber,
});
setSuccess(true);
setTimeout(() => navigate('/verify-email', { state: { email: formData.email } }), 2000);
} catch (err) {
setError(err.response?.data?.message || 'Registration failed');
} finally {
setLoading(false);
}
};

if (success) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full p-8 bg-white rounded-lg shadow text-center">
<div className="text-green-600 text-5xl mb-4">βœ“</div>
<h2 className="text-2xl font-bold mb-2">Registration Successful!</h2>
<p className="text-gray-600">Check your email for verification code.</p>
</div>
</div>
);
}

return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
<div>
<h2 className="text-3xl font-bold text-center">Create Account</h2>
</div>

{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded">
{error}
</div>
)}

<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700">
Name
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>

<div>
<label className="block text-sm font-medium text-gray-700">
Email
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>

<div>
<label className="block text-sm font-medium text-gray-700">
Phone Number (Optional)
</label>
<input
type="tel"
name="phoneNumber"
value={formData.phoneNumber}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>

<div>
<label className="block text-sm font-medium text-gray-700">
Password
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
required
minLength={8}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>

<div>
<label className="block text-sm font-medium text-gray-700">
Confirm Password
</label>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>

<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Creating account...' : 'Sign up'}
</button>
</form>

<div className="text-center">
<span className="text-sm text-gray-600">Already have an account? </span>
<Link to="/login" className="text-sm text-blue-600 hover:underline">
Sign in
</Link>
</div>
</div>
</div>
);
}

7. Email Verification Component​

// src/pages/VerifyEmail.jsx
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

export function VerifyEmail() {
const [otp, setOtp] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);

const { verifyEmail } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const email = location.state?.email;

const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);

try {
await verifyEmail(email, otp);
navigate('/login');
} catch (err) {
setError(err.response?.data?.message || 'Verification failed');
} finally {
setLoading(false);
}
};

return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
<div>
<h2 className="text-3xl font-bold text-center">Verify Email</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Enter the 6-digit code sent to {email}
</p>
</div>

{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded">
{error}
</div>
)}

<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700">
Verification Code
</label>
<input
type="text"
value={otp}
onChange={(e) => setOtp(e.target.value)}
required
maxLength={6}
placeholder="000000"
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md text-center text-2xl tracking-widest"
/>
</div>

<button
type="submit"
disabled={loading || otp.length !== 6}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Verifying...' : 'Verify Email'}
</button>
</form>
</div>
</div>
);
}

8. Dashboard Component​

// src/pages/Dashboard.jsx
import { useAuth } from '../context/AuthContext';

export function Dashboard() {
const { user, logout, hasRole, hasPermission } = useAuth();

return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-bold">Dashboard</h1>
</div>
<div className="flex items-center space-x-4">
<span className="text-gray-700">{user?.name}</span>
<button
onClick={logout}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Logout
</button>
</div>
</div>
</div>
</nav>

<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<div className="bg-white shadow rounded-lg p-6">
<h2 className="text-2xl font-bold mb-4">Welcome, {user?.name}!</h2>

<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold">User Information</h3>
<dl className="mt-2 space-y-2">
<div>
<dt className="text-sm font-medium text-gray-500">Email</dt>
<dd className="text-sm text-gray-900">{user?.email}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Email Verified</dt>
<dd className="text-sm text-gray-900">
{user?.emailVerified ? (
<span className="text-green-600">βœ“ Verified</span>
) : (
<span className="text-red-600">βœ— Not Verified</span>
)}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">MFA Enabled</dt>
<dd className="text-sm text-gray-900">
{user?.mfaEnabled ? (
<span className="text-green-600">βœ“ Enabled</span>
) : (
<span className="text-gray-600">βœ— Disabled</span>
)}
</dd>
</div>
</dl>
</div>

<div>
<h3 className="text-lg font-semibold">Roles</h3>
<div className="mt-2 flex flex-wrap gap-2">
{user?.roles?.map((role) => (
<span
key={role}
className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm"
>
{role}
</span>
))}
</div>
</div>

<div>
<h3 className="text-lg font-semibold">Permissions</h3>
<div className="mt-2 flex flex-wrap gap-2">
{user?.permissions?.map((permission) => (
<span
key={permission}
className="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm"
>
{permission}
</span>
))}
</div>
</div>

{hasRole('ADMIN') && (
<div className="mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded">
<h3 className="text-lg font-semibold text-yellow-800">
Admin Section
</h3>
<p className="text-sm text-yellow-700 mt-2">
You have admin privileges and can access admin features.
</p>
</div>
)}

{hasPermission('user:create') && (
<div className="mt-6 p-4 bg-purple-50 border border-purple-200 rounded">
<h3 className="text-lg font-semibold text-purple-800">
User Management
</h3>
<p className="text-sm text-purple-700 mt-2">
You can create and manage users.
</p>
</div>
)}
</div>
</div>
</div>
</main>
</div>
);
}

9. App Router Setup​

// src/App.jsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { Login } from './pages/Login';
import { Register } from './pages/Register';
import { VerifyEmail } from './pages/VerifyEmail';
import { Dashboard } from './pages/Dashboard';

function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/verify-email" element={<VerifyEmail />} />

<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>

<Route
path="/admin"
element={
<ProtectedRoute roles={['ADMIN']}>
<div>Admin Page</div>
</ProtectedRoute>
}
/>

<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/unauthorized" element={<div>Unauthorized</div>} />
</Routes>
</AuthProvider>
</BrowserRouter>
);
}

export default App;

Security Best Practices​

Password Security​

// Password validation
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$",
message = "Password must contain at least 8 characters, one uppercase, one lowercase, one number and one special character"
)
private String password;

// Password strength checker (JavaScript)
function checkPasswordStrength(password) {
let strength = 0;
if (password.length >= 8) strength++;
if (password.length >= 12) strength++;
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
if (/\d/.test(password)) strength++;
if (/[@$!%*?&]/.test(password)) strength++;

const levels = ['Weak', 'Fair', 'Good', 'Strong', 'Very Strong'];
return levels[strength];
}

Token Security​

Best Practices:

  • βœ… Use HTTPS in production
  • βœ… Short-lived access tokens (15 min)
  • βœ… Long-lived refresh tokens (7-30 days)
  • βœ… Store refresh tokens in HttpOnly cookies
  • βœ… Implement token rotation
  • βœ… Use secure random secrets (256-bit minimum)
  • βœ… Implement token blacklisting for logout

API Security​

// Rate limiting with Bucket4j
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String key = request.getRemoteAddr();
Bucket bucket = buckets.computeIfAbsent(key, k -> createBucket());

if (bucket.tryConsume(1)) {
return true;
}

response.setStatus(429);
response.getWriter().write("{\"error\": \"Too many requests\"}");
return false;
}

private Bucket createBucket() {
return Bucket.builder()
.addLimit(Bandwidth.simple(100, Duration.ofMinutes(1)))
.build();
}
}

Common Vulnerabilities​

VulnerabilityProtection
XSSHttpOnly cookies, CSP headers, input sanitization
CSRFSameSite cookies, CSRF tokens
SQL InjectionParameterized queries, ORM
Brute ForceRate limiting, account lockout, CAPTCHA
Session FixationRegenerate session on login
Man-in-the-MiddleHTTPS only, secure cookies

Production Checklist​

Environment Variables​

# Production application.properties
server.port=8080
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=${SSL_PASSWORD}
server.ssl.key-store-type=PKCS12

spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}

jwt.secret=${JWT_SECRET}
jwt.access-token-expiration=900000
jwt.refresh-token-expiration=604800000

cookie.secure=true
cookie.http-only=true
cookie.same-site=Strict

spring.mail.host=${MAIL_HOST}
spring.mail.port=${MAIL_PORT}
spring.mail.username=${MAIL_USERNAME}
spring.mail.password=${MAIL_PASSWORD}

Deployment Checklist​

  • βœ… Enable HTTPS/SSL
  • βœ… Set secure cookie flags
  • βœ… Use environment variables for secrets
  • βœ… Enable CORS for specific origins only
  • βœ… Implement rate limiting
  • βœ… Set up monitoring and logging
  • βœ… Enable database encryption
  • βœ… Use password strength requirements
  • βœ… Implement account lockout policies
  • βœ… Set up automated backups
  • βœ… Enable audit logging
  • βœ… Configure firewall rules
  • βœ… Use CDN for static assets
  • βœ… Implement health checks
  • βœ… Set up error tracking (Sentry, etc.)

Conclusion​

This guide provides a complete, production-ready authentication and authorization system using Spring Boot and React. The implementation includes JWT tokens, RBAC, MFA, OTP verification, and follows security best practices.

Key Features:

  • βœ… Secure JWT authentication with refresh tokens
  • βœ… Role-Based Access Control (RBAC)
  • βœ… Multi-Factor Authentication (MFA)
  • βœ… Email verification with OTP
  • βœ… Password reset functionality
  • βœ… HttpOnly cookies for token storage
  • βœ… Protected routes and API endpoints
  • βœ… Security best practices implemented

Next Steps:

  • Implement OAuth2 SSO (Google, GitHub)
  • Add biometric authentication
  • Implement passwordless authentication
  • Add session management dashboard
  • Set up audit logging
  • Implement device fingerprinting
  • Add backup codes for MFA recovery

For production deployment, ensure all security measures are in place and thoroughly test all authentication flows.