JWT json web token - used for authentication
instead of storing login sessions on the server, the client sends token with every request
flow
User Login -> Server verifies credentials -> Server creates JWT -> Clients stores JWT
Client sends JWT in request -> Server verifies and grants access
Authorization: Bearer token
Jwt structure
HEADER.PAYLOAD.SIGNATURE jwt.io
Header contains algorithm:
{
"alg":"HS256"
"typ":"JWT"
}
payload :
{
"sub":"a@c.com",
"iat":"8765456,
"exp":98765456
}
Signature: created using a scret key
Simplified flow in Spring security
SPRING SECURITY + JWT AUTHENTICATION FLOW
=========================================
JWT (JSON Web Token)
Used for authentication in stateless APIs.
Instead of storing login sessions on the server,
the client sends a token with every request.
---------------------------------------------------
1. USER REGISTRATION FLOW
---------------------------------------------------
User
│
│ POST /api/auth/register
│ Sends:
│ { fullName, email, password, role }
▼
AuthController.registerUser()
│
▼
AuthService.registerUser()
│
│ Check if user already exists
│ userRepository.existsByUsername(email)
│
│ Encrypt password
│ passwordEncoder.encode(password)
│
│ Create new User entity
│
▼
UserRepository.save(user)
│
▼
Database
(application_user table)
Result:
User is stored with a hashed password (BCrypt)
---------------------------------------------------
2. USER LOGIN FLOW
---------------------------------------------------
User
│
│ POST /api/auth/login
│ Sends:
│ { username, password }
▼
AuthController.login()
│
▼
AuthService.login()
│
│ Create authentication request
│ UsernamePasswordAuthenticationToken
│
▼
AuthenticationManager.authenticate()
│
▼
DaoAuthenticationProvider
│
▼
CustomDetailsService.loadUserByUsername()
│
│ Fetch user from database
│ userRepository.findByEmail(username)
│
▼
Database
│
▼
UserDetails returned to Spring Security
│
│ Spring Security compares:
│ entered password
│ vs
│ hashed password in DB
│ using BCryptPasswordEncoder
│
▼
If credentials are correct
│
▼
Authentication object created
│
▼
SecurityContextHolder
stores authentication
This represents the currently logged-in user.
---------------------------------------------------
3. JWT TOKEN GENERATION
---------------------------------------------------
AuthService.login()
│
▼
JwtService.generateTokenPair(authentication)
│
├─ generateAccessToken()
│
└─ generateRefreshToken()
│
▼
JwtService.generateToken()
JWT Structure:
HEADER.PAYLOAD.SIGNATURE
HEADER
{
"alg": "HS256",
"typ": "JWT"
}
PAYLOAD (claims)
{
"sub": "user@email.com",
"iat": 1710000000,
"exp": 1710003600
}
SIGNATURE
HMACSHA256(
base64(header) + "." + base64(payload),
secret_key
)
Secret key comes from:
application.properties
app.jwt.secret=...
app.jwt.expiration=3600000
app.jwt.refresh-expiration=86400000
Result returned to client:
{
accessToken: "...",
refreshToken: "..."
}
---------------------------------------------------
4. CLIENT STORES TOKEN
---------------------------------------------------
Client stores JWT
Possible storage locations:
- LocalStorage
- SessionStorage
- Memory
- Secure cookies
Example header for future requests:
Authorization: Bearer <access_token>
---------------------------------------------------
5. ACCESSING PROTECTED APIs
---------------------------------------------------
Client request
GET /api/orders
Header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Request enters Spring Security
---------------------------------------------------
6. SPRING SECURITY FILTER CHAIN
---------------------------------------------------
Client Request
│
▼
SecurityFilterChain
│
▼
JwtAuthenticationFilter
│
│ Extract token from header
│ Authorization: Bearer token
│
▼
JwtService.isValidToken(token)
│
│ Validate signature using secret key
│
▼
JwtService.extractUsernameFromToken()
│
▼
CustomDetailsService.loadUserByUsername()
│
▼
Database
UserDetails returned
│
▼
Create Authentication object
UsernamePasswordAuthenticationToken
│
▼
SecurityContextHolder.setAuthentication()
Now Spring Security considers the user AUTHENTICATED.
---------------------------------------------------
7. CONTROLLER EXECUTION
---------------------------------------------------
Request continues to controller:
Example:
OrderController.getOrders()
Because authentication exists in SecurityContext,
the endpoint is allowed.
---------------------------------------------------
8. ACCESS TOKEN EXPIRATION
---------------------------------------------------
Access tokens are short-lived.
Example:
app.jwt.expiration=3600000
= 1 hour
If token expires:
Server returns:
401 Unauthorized
---------------------------------------------------
9. REFRESH TOKEN FLOW
---------------------------------------------------
Client sends:
POST /api/auth/refresh-token
Body:
{
refreshToken: "..."
}
AuthController.refreshToken()
│
▼
AuthService.refreshToken()
│
▼
JwtService.isRefreshToken()
│
│ Check claim:
│ tokenType = refresh
│
▼
JwtService.extractUsernameFromToken()
│
▼
CustomDetailsService.loadUserByUsername()
│
▼
Create Authentication object
│
▼
JwtService.generateAccessToken()
│
▼
Return new access token
Response:
{
accessToken: "new_access_token",
refreshToken: "existing_refresh_token"
}
---------------------------------------------------
10. WHY THIS IS STATELESS
---------------------------------------------------
Traditional authentication:
Client
│
▼
Server stores SESSION
│
▼
Client sends SESSION ID cookie
JWT authentication:
Client
│
▼
Server issues TOKEN
│
▼
Client stores token
│
▼
Client sends token in every request
│
▼
Server only verifies token signature
Server does NOT store sessions.
---------------------------------------------------
11. IMPORTANT SPRING SECURITY COMPONENTS
---------------------------------------------------
SecurityFilterChain
Defines security rules for HTTP requests.
AuthenticationManager
Responsible for authenticating users.
DaoAuthenticationProvider
Uses UserDetailsService to load users.
UserDetailsService
Loads users from database.
SecurityContextHolder
Stores authenticated user for current request.
JwtAuthenticationFilter
Intercepts requests and validates JWT tokens.
JwtService
Creates and verifies JWT tokens.
UserRepository
Fetches users from database.
AuthService
Handles login, registration, and refresh token logic.
---------------------------------------------------
12. COMPLETE REQUEST FLOW SUMMARY
---------------------------------------------------
User Login
│
▼
Spring Security authenticates credentials
│
▼
JWT tokens generated
│
▼
Client stores tokens
│
▼
Client sends token with each request
│
▼
JwtAuthenticationFilter validates token
│
▼
SecurityContext updated
│
▼
Controller executes
│
▼
Response returned
Dependecies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
config in properties
# JWT Configuration
app.jwt.secret=856B4FEB2CF84D145A231D7A28B41856B4FEB2CF84D145A231D7A28B41856B4FEB2CF84D145A231D7A28B41
app.jwt.expiration=3600000
app.jwt.refresh-expiration=86400000
Create user
User entity
User.java
package com.grazac.wisdom.user;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "application_user", indexes = {
@Index(name = "idx_user_email", columnList = "email")
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(
name = "full_name",
nullable = false
)
private String fullName;
@Column(
name = "email",
unique = true,
nullable = false
)
private String email;
@Column(
nullable = false,
unique = true
)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = true)
private String profilePic;
@Column(nullable = true)
private String accountNumber;
@Enumerated(EnumType.STRING)
private Role role;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
public User(String fullName, String username, String password, Role role) {
this.fullName = fullName;
this.username = username;
this.password = password;
this.role = role;
}
}
Role.enum
package com.grazac.wisdom.user;
public enum Role {
ROLE_USER,
ROLE_ADMIN
}
UserRepository
package com.grazac.wisdom.user;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Boolean existsByUsername(String username);
Optional<User> findByEmail(String username);
}
CurrentUser
@Component // Marks this class as a Spring Bean so Spring can automatically detect and manage it
public class CurrentUserUtil {
// Repository used to fetch the User entity from the database
private final UserRepository userRepository;
// Constructor injection: Spring automatically injects the UserRepository bean here
public CurrentUserUtil(UserRepository userRepository) {
this.userRepository = userRepository;
}
// Method to get the username of the currently authenticated user
public String getLoggedInUsername() {
// SecurityContextHolder stores security information for the current request/thread
// getContext() returns the current security context
// getAuthentication() returns the authentication object of the logged-in user
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// getName() returns the username (principal name) of the authenticated user
return authentication.getName();
}
// Method to get the full User entity of the currently logged-in user
public User getLoggedInUser() {
// Get the username from the security context
String username = getLoggedInUsername();
// Search for the user in the database using the username
return userRepository.findByUsername(username)
// If the user is not found, throw a custom exception
.orElseThrow(() -> new UserNotFoundException("User not found: " + username));
}
}
in utils
ApiResponse.java
import lombok.Data;
@Data
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private ApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(true, message, data);
}
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "Request successful", data);
}
public static <T> ApiResponse<T> fail(String message) {
return new ApiResponse<>(false, message, null);
}
public static <T> ApiResponse<T> fail(String message, T data) {
return new ApiResponse<>(false, message, data);
}
}
CustomDetailsService.java
package com.grazac.wisdom.user;
// Declares the package where this class belongs
import org.springframework.security.core.GrantedAuthority;
// Represents a permission or role granted to a user
import org.springframework.security.core.authority.SimpleGrantedAuthority;
// A basic implementation of GrantedAuthority used for roles like ROLE_ADMIN
import org.springframework.security.core.userdetails.UserDetails;
// Spring Security interface that represents an authenticated user
import org.springframework.security.core.userdetails.UserDetailsService;
// Interface used by Spring Security to load user-specific data during login
import org.springframework.security.core.userdetails.UsernameNotFoundException;
// Exception thrown when a user cannot be found during authentication
import org.springframework.stereotype.Service;
// Marks this class as a Spring service bean so it can be automatically detected
import java.util.Collection;
// Represents a group of objects (used for authorities)
import java.util.List;
// Used to create a list of authorities
@Service
// Registers this class as a Spring-managed bean so Spring Security can use it
public class CustomDetailsService implements UserDetailsService {
// Repository used to fetch users from the database
private final UserRepository userRepository;
// Constructor injection: Spring automatically injects UserRepository here
public CustomDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
// Method required by UserDetailsService
// Spring Security calls this method automatically during login
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// Search for the user in the database using email
// If the user does not exist, throw UsernameNotFoundException
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// Return a Spring Security User object
// This object contains authentication information used by Spring Security
return new org.springframework.security.core.userdetails.User(
// Username used by Spring Security (principal)
user.getUsername(),
// Password stored in the database (usually encrypted with BCrypt)
user.getPassword(),
// List of roles/authorities granted to the user
getAuthority(user)
);
}
// Method to convert the user's role into a Spring Security authority
private Collection<? extends GrantedAuthority> getAuthority(User user) {
// Create an authority using the role name from the User entity
// Example: ROLE_ADMIN or ROLE_USER
GrantedAuthority authority = new SimpleGrantedAuthority(user.getRole().name());
// Return the authority as a list (Spring expects a collection)
return List.of(authority);
}
}
Token Pair
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class TokenPair {
private String accessToken;
private String refreshToken;
public TokenPair() {
}
public TokenPair(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
JwtService.java
package com.grazac.wisdom.user;
// Defines the package where this class belongs
import dto.TokenPair;
// Custom DTO used to return both access token and refresh token together
import io.jsonwebtoken.*;
// JWT library classes for building and parsing tokens
import io.jsonwebtoken.io.Decoders;
// Utility class for decoding Base64 encoded secrets
import io.jsonwebtoken.security.Keys;
// Used to generate cryptographic signing keys
import lombok.extern.slf4j.Slf4j;
// Lombok annotation that automatically creates a logger
import org.springframework.beans.factory.annotation.Value;
// Used to inject values from application.properties or application.yml
import org.springframework.security.core.Authentication;
// Represents the authenticated user in Spring Security
import org.springframework.security.core.userdetails.UserDetails;
// Interface representing user information in Spring Security
import org.springframework.stereotype.Service;
// Marks this class as a Spring service bean
import javax.crypto.SecretKey;
// Represents the secret key used to sign the JWT
import java.util.Date;
// Used for token creation time and expiration time
import java.util.HashMap;
import java.util.Map;
@Service
// Registers this class as a Spring-managed service
@Slf4j
// Adds a logger automatically (log.info(), log.error(), etc.)
public class JwtService {
// Inject the JWT secret key from application.properties
@Value("${app.jwt.secret}")
private String jwtSecret;
// Access token expiration time (milliseconds)
@Value("${app.jwt.expiration}")
private long jwtExpirationMs;
// Refresh token expiration time (milliseconds)
@Value("${app.jwt.refresh-expiration}")
private long refreshExpirationMs;
// Generates both access and refresh tokens for a user
public TokenPair generateTokenPair(Authentication authentication) {
// Create access token
String accessToken = generateAccessToken(authentication);
// Create refresh token
String refreshToken = generateRefreshToken(authentication);
// Return both tokens wrapped in a DTO
return new TokenPair(accessToken, refreshToken);
}
// Authentication here -> refers to Spring authentication object that represents the current authenticated users(an interface). Usually holds
| Field | Meaning |
| ----------------- | ----------------------------- |
| **principal** | the user (UserDetails object) |
| **credentials** | password or token |
| **authorities** | roles like ROLE_ADMIN |
| **authenticated** | whether login succeeded |
// Generate short-lived access token
public String generateAccessToken(Authentication authentication) {
// Calls general token generator with normal expiration
return generateToken(authentication, jwtExpirationMs, new HashMap<>());
}
// Generate refresh token (longer expiration)
public String generateRefreshToken(Authentication authentication) {
// Create claims map to store extra data in the token
Map<String, String> claims = new HashMap<>();
// Mark token as refresh token
claims.put("tokenType", "refresh");
// Generate token with refresh expiration time
return generateToken(authentication, refreshExpirationMs, claims);
}
// Generic method used to generate any token
public String generateToken(Authentication authentication, long expirationInMs, Map<String, String> claims) {
// Extract logged-in user from authentication object
UserDetails userPrincipal = (UserDetails) authentication.getPrincipal();
// Current time
Date now = new Date();
// Token expiration time
Date expiryDate = new Date(now.getTime() + expirationInMs);
// Build the JWT token
return Jwts.builder()
// Add header information
.header()
.add("typ", "JWT")
.and()
// Subject is usually the username
.subject(userPrincipal.getUsername())
// Add custom claims (e.g. tokenType)
.claims(claims)
// Time token was issued
.issuedAt(now)
// Expiration time
.expiration(expiryDate)
// Sign the token using the secret key
.signWith(getSignInKey())
// Build the token string
.compact();
}
// Validate token by checking username matches the user
public boolean validateTokenForUser(String token, UserDetails userDetails) {
// Extract username stored in the token
final String username = extractUsernameFromToken(token);
// Token is valid if username matches the user
return username != null
&& username.equals(userDetails.getUsername());
}
// Checks if token is valid (not malformed or expired)
public boolean isValidToken(String token) {
// If claims can be extracted, token is valid
return extractAllClaims(token) != null;
}
// Extract username from token
public String extractUsernameFromToken(String token) {
// Get all token claims
Claims claims = extractAllClaims(token);
if(claims != null) {
// Subject contains the username
return claims.getSubject();
}
return null;
}
// Check if token is specifically a refresh token
public boolean isRefreshToken(String token) {
Claims claims = extractAllClaims(token);
if(claims == null) {
return false;
}
// Check custom claim "tokenType"
return "refresh".equals(claims.get("tokenType"));
}
// Extract all claims from the token
private Claims extractAllClaims(String token) {
Claims claims = null;
try {
// Parse and validate the JWT
claims = Jwts.parser()
.verifyWith(getSignInKey()) // verify using secret key
.build()
.parseSignedClaims(token) // parse token
.getPayload(); // extract claims
} catch (JwtException | IllegalArgumentException e) {
// Token invalid or malformed
throw new RuntimeException(e);
}
return claims;
}
// Generate signing key from secret
private SecretKey getSignInKey() {
// Decode the base64 secret
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
// Create HMAC SHA signing key
return Keys.hmacShaKeyFor(keyBytes);
}
// Calculate how much time remains before token expires
public long getRemainingValidity(String token) {
Claims claims = extractAllClaims(token);
// Return remaining milliseconds before expiration
return claims.getExpiration().getTime() - System.currentTimeMillis();
}
}
RegisterRequest.java
package dto;
import com.grazac.wisdom.user.Role;
import lombok.*;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class RegisterRequest {
private String fullName;
private String email;
private String password;
private Role role;
}
LoginRequest
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest {
private String username;
private String password;
}
RefreshTokenRequest
@Data
@AllArgsConstructor
public class RefreshTokenRequest {
private String refreshToken;
}
SecurityConfig.java
package com.grazac.wisdom.utils;
// Import custom JWT filter that reads JWT from incoming requests
import com.grazac.wisdom.filters.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// Spring Security authentication components
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
// Provides AuthenticationManager configured by Spring
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
// Enables method level security like @PreAuthorize
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
// Used to configure HTTP security
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
// Enables Spring Security for the application
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
// Controls session policy (STATELESS for JWT)
import org.springframework.security.config.http.SessionCreationPolicy;
// Used to load users from database
import org.springframework.security.core.userdetails.UserDetailsService;
// Password hashing using BCrypt
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
// Security filter chain
import org.springframework.security.web.SecurityFilterChain;
// Default authentication filter used by Spring
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
// CORS configuration imports
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
// Marks this class as a configuration class for Spring
@EnableWebSecurity
// Enables Spring Security in the application
@EnableMethodSecurity
// Allows security annotations like @PreAuthorize
public class SecurityConfig {
// Service that loads users from the database
private final UserDetailsService userDetailsService;
// Custom filter that validates JWT tokens
private final JwtAuthenticationFilter jwtAuthenticationFilter;
// Constructor injection
public SecurityConfig(UserDetailsService userDetailsService,
JwtAuthenticationFilter jwtAuthenticationFilter) {
this.userDetailsService = userDetailsService;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
// List of endpoints that do NOT require authentication
public static final String[] WHITE_LIST_URL = {
"/togglz-console", "/togglz-console/**",
"/api/terminals/**",
"/api/courses/**",
"/ws/**",
"/api/posts",
"/electronics/**",
"/activity/**",
"/actuator/**",
"/login/**",
"/booking/**",
"/login/oauth2/code/google",
"/api/auth/**",
"/api/orders/**",
"/api/sample/**",
"/api/external/**",
"/info",
"/blogs/**",
"/posts/**",
// Swagger documentation endpoints
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui/**",
"/webjars/**",
"/swagger-ui.html",
"/error"
};
// Password encoder bean
// BCrypt hashes passwords before saving to the database
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// It connects Spring Security’s login system to your database + password hashing.
// Authentication provider that uses database users
// DaoAuthenticationProvider → uses your UserDetailsService to load the user from the DB.
// setPasswordEncoder(...) → verifies the hashed password correctly.
// AuthenticationProvider bean → registers this logic so Spring Security can handle login requests.
@Bean
public AuthenticationProvider authenticationProvider() {
// DaoAuthenticationProvider loads user using UserDetailsService
DaoAuthenticationProvider authProvider =
new DaoAuthenticationProvider(userDetailsService);
// Set password encoder for verifying passwords
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
// AuthenticationManager used for login authentication
// This exposes **Spring Security’s AuthenticationManager as a bean so you can use it to authenticate login requests manually (e.g., in a login controller).
// so that we can:
// authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
// Returns Spring's default AuthenticationManager
return authConfig.getAuthenticationManager();
}
// Main Spring Security configuration
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Enable CORS using our configuration
// Allows requests from other origins (like your frontend).
// Backend → api.example.com
// Frontend → localhost:3000
.cors(c -> c.configurationSource(corsConfigurationSource()))
// Disable CSRF because we are using JWT (stateless authentication)
.csrf(csrf -> csrf.disable())
// Authorization rules
.authorizeHttpRequests(
req -> req
// Allow all requests to white-listed endpoints
.requestMatchers(WHITE_LIST_URL).permitAll()
// Any other endpoint requires authentication
.anyRequest().authenticated()
)
// Configure session management
.sessionManagement(
session -> session
// Stateless because JWT handles authentication
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// Add JWT filter before default Spring login filter
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
)
// Use our custom authentication provider
.authenticationProvider(authenticationProvider());
// Build the filter chain
return http.build();
}
// Configure CORS (Cross-Origin Resource Sharing)
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// Allowed frontend applications
configuration.setAllowedOrigins(List.of(
"http://localhost:63342",
"http://localhost:3000",
"https://secsystem-emr.vercel.app",
"https://emr-sigma-ten.vercel.app",
"Postman"
));
// Allowed HTTP methods
configuration.setAllowedMethods(List.of(
"GET",
"POST",
"PUT",
"DELETE",
"OPTIONS"
));
// Allowed request headers
configuration.setAllowedHeaders(List.of(
"Authorization",
"Content-Type"
));
// Allow sending Authorization header or cookies
configuration.setAllowCredentials(true);
// Apply this CORS config to all endpoints
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
JwtAuthenticationFilter.java
package com.grazac.wisdom.filters;
import com.grazac.wisdom.user.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import tools.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Map;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal
(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return; // stop here
}
jwt = getJwtFromRequest(request);
if (!jwtService.isValidToken(jwt)) {
filterChain.doFilter(request, response);
return;
}
try {
username = jwtService.extractUsernameFromToken(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.validateTokenForUser(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
} catch (UsernameNotFoundException ex) {
sendErrorResponse(response, "Unauthorized", HttpStatus.UNAUTHORIZED);
} catch (Exception ex) {
sendErrorResponse(response, "Authentication error: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
private String getJwtFromRequest(HttpServletRequest request) {
final String authHeader = request.getHeader("Authorization");
return authHeader.substring(7); // Remove "Bearer "
}
private void sendErrorResponse(HttpServletResponse response, String message, HttpStatus status) throws IOException {
response.setStatus(status.value());
response.setContentType("application/json");
Map<String, Object> body = Map.of(
"timestamp", LocalDateTime.now().toString(),
"status", status.value(),
"error", message
);
new ObjectMapper().writeValue(response.getWriter(), body);
}
}
AuthService
package com.grazac.wisdom.service;
import com.grazac.wisdom.exceptions.ConflictException;
import com.grazac.wisdom.user.CurrentUserUtil;
import com.grazac.wisdom.user.JwtService;
import com.grazac.wisdom.user.User;
import com.grazac.wisdom.user.UserRepository;
import dto.LoginRequest;
import dto.RefreshTokenRequest;
import dto.RegisterRequest;
import dto.TokenPair;
import jakarta.transaction.Transactional;
import lombok.AllArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
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.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Service
@AllArgsConstructor
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
private final CurrentUserUtil currentUserUtil;
@Transactional
public void registerUser(RegisterRequest registerRequest) {
// Check if user with the same username already exist
if(userRepository.existsByUsername(registerRequest.getEmail())) {
throw new ConflictException("Username is already in use");
}
// Create new user
User user = User
.builder()
.createdAt(LocalDateTime.now())
.fullName(registerRequest.getFullName())
.username(registerRequest.getEmail())
.email(registerRequest.getEmail())
.password(passwordEncoder.encode(registerRequest.getPassword()))
.role(registerRequest.getRole())
.build();
userRepository.save(user);
}
public TokenPair login(LoginRequest loginRequest) {
// Ask Spring Security to authenticate the user
Authentication authentication = authenticationManager.authenticate(
// Create an authentication request object containing username and password
new UsernamePasswordAuthenticationToken(
// Username from the login request
loginRequest.getUsername(),
// Password from the login request
loginRequest.getPassword()
)
);
// If authentication succeeds, store the authentication object
// in Spring Security's SecurityContext (represents the logged-in user)
SecurityContextHolder.getContext().setAuthentication(authentication);
// Generate both Access Token and Refresh Token for the authenticated user
return jwtService.generateTokenPair(authentication);
}
public TokenPair refreshToken(RefreshTokenRequest request) {
// Extract the refresh token sent by the client
String refreshToken = request.getRefreshToken();
// Check if the token is actually a refresh token
// (your JwtService checks the claim "tokenType = refresh")
if(!jwtService.isRefreshToken(refreshToken)) {
// If not a refresh token, reject the request
throw new IllegalArgumentException("Invalid refresh token");
}
// Extract the username stored inside the token
String user = jwtService.extractUsernameFromToken(refreshToken);
// Load the user details from the database
UserDetails userDetails = userDetailsService.loadUserByUsername(user);
// If user cannot be found, stop the process
if (userDetails == null) {
throw new IllegalArgumentException("User not found");
}
// Create an authentication object manually
// (this represents an authenticated user without needing password)
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
// Authenticated user
userDetails,
// No password required since token already verified
null,
// User roles/authorities
userDetails.getAuthorities()
);
// Generate a new access token using the authenticated user
String accessToken = jwtService.generateAccessToken(authentication);
// Return the new access token and the existing refresh token
return new TokenPair(accessToken, refreshToken);
}
}
AuthController
package com.grazac.wisdom;
import com.grazac.wisdom.service.AuthService;
import com.grazac.wisdom.utils.ApiResponse;
import dto.LoginRequest;
import dto.RefreshTokenRequest;
import dto.RegisterRequest;
import dto.TokenPair;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/register")
public ResponseEntity<ApiResponse<String>> registerUser(@RequestBody RegisterRequest request) {
// Save the new user to the database and return success response.
authService.registerUser(request);
return ResponseEntity.ok(ApiResponse.success("User registered successfully"));
}
@PostMapping("/login")
public ResponseEntity<ApiResponse<Object>> login(@RequestBody LoginRequest loginRequest) {
TokenPair tokenPair = authService.login(loginRequest);
return ResponseEntity.ok(ApiResponse.success(tokenPair));
}
@PostMapping("/refresh-token")
public ResponseEntity<?> refreshToken( @RequestBody RefreshTokenRequest request) {
TokenPair tokenPair = authService.refreshToken(request);
return ResponseEntity.ok(tokenPair);
}
}
Top comments (0)