DEV Community

DrSimple
DrSimple

Posted on • Edited on

Java Security

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

config in properties


# JWT Configuration
app.jwt.secret=856B4FEB2CF84D145A231D7A28B41856B4FEB2CF84D145A231D7A28B41856B4FEB2CF84D145A231D7A28B41
app.jwt.expiration=3600000
app.jwt.refresh-expiration=86400000

Enter fullscreen mode Exit fullscreen mode

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

Role.enum

package com.grazac.wisdom.user;

public enum Role {
    ROLE_USER,
    ROLE_ADMIN
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

LoginRequest

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest {
    private String username;
    private String password;
}
Enter fullscreen mode Exit fullscreen mode

RefreshTokenRequest

@Data
@AllArgsConstructor
public class RefreshTokenRequest {
    private String refreshToken;
}
Enter fullscreen mode Exit fullscreen mode

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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }

}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)