Skip to content

harman-04/spring-boot-security-jwt-stateless-refresh-tokens

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Spring Boot: Stateless JWT Authentication

Project Overview

In a distributed microservices environment, storing user sessions in memory is impossible. This project implements JSON Web Tokens (JWT), allowing the server to verify user identity without maintaining any server-side state.


Technical Concepts

1. The JWT Lifecycle

  • Generation: When a user logs in via /auth/generateToken, the JwtService creates a signed string containing the username and expiration date.
  • Signing: We use the HMAC-SHA algorithm with a secret key. This ensures that if a user tries to change their username inside the token, the signature becomes invalid.
  • Storage: The client (Frontend) saves this token.

2. Custom Security Filter (JwtAuthFilter)

This is the "bouncer" of the application. It extends OncePerRequestFilter:

  1. It intercepts every incoming request.
  2. It looks for the Authorization: Bearer <token> header.
  3. If found, it validates the token with JwtService.
  4. If valid, it manually populates the SecurityContextHolder so Spring knows the user is authenticated for that specific request.

3. Stateless Session Management

In SecurityConfig.java, we set: .sessionCreationPolicy(SessionCreationPolicy.STATELESS) This tells Spring Security never to create an HTTPSession. Every single request must provide a valid JWT to be processed.


Component Reference

JwtService.java

The utility class responsible for:

  • Creating the token with a 30-minute expiration.
  • Extracting the "Subject" (username) from the token.
  • Verifying if the token has expired or been tampered with.

UserInfoService.java & UserInfoDetails.java

These bridge our MySQL database with Spring Security. UserInfoDetails converts our UserInfo entity into something Spring Security understands, including converting a comma-separated string of roles (e.g., "ROLE_USER, ROLE_ADMIN") into GrantedAuthority objects.


Key Concepts Explained Simply

UserInfo

  • @Entity: Maps this class to a database table.
  • @Id + @GeneratedValue: Defines the primary key (id) with auto-increment.
  • Fields:
    • name: Display name.
    • email: Often used as the username for login.
    • password: Stored securely (hashed with BCrypt).
    • roles: Defines user’s authorities (e.g., ROLE_USER, ROLE_ADMIN).
  • Lombok Annotations:
    • @Data: Auto-generates getters/setters, equals, hashCode, toString.
    • @AllArgsConstructor: Constructor with all fields.
    • @NoArgsConstructor: Default constructor.

AuthRequest

  • Purpose of AuthRequest:
    • Acts as a Data Transfer Object (DTO) for login requests.
    • When a user sends credentials (username + password) via a REST API, this class captures them.
  • Integration in JWT Flow:
    • User sends a POST request to /auth/generateToken with JSON body:
    {
     "username": "harman",
     "password": "123"
     }
    • Spring Boot maps this JSON into an AuthRequest object automatically.
    • Authentication logic checks the credentials against the database (UserInfo).
    • If valid: a JWT token is generated and returned.
    • If invalid: authentication fails with 401 Unauthorized.
  • Lombok Benefits:
    • Saves boilerplate code (no need to manually write getters/setters).
    • Keeps the class clean and focused on its purpose.

UserInfoRepository

  • JpaRepository<UserInfo, Long>:
    • Provides built-in CRUD methods: save(), findById(), findAll(), deleteById(), etc.
    • UserInfo: entity type.
    • Long: primary key type (id).
  • Custom Finder Method (findByEmail):
    • Spring Data JPA parses the method name and auto-generates the query.
    • No need to write SQL manually.
    • Useful for authentication to load user by email.
  • Optional<UserInfo>:
    • Prevents NullPointerException.
    • Forces you to handle the case where no user is found.

UserInfoDetails

  • UserDetails: Spring Security’s standard representation of a user.
  • UserInfoDetails: Custom adapter that converts your UserInfo entity into UserDetails.
  • Authorities: Roles like ROLE_USER, ROLE_ADMIN converted into GrantedAuthority.
  • Password: Stored as a hashed value (BCrypt).
  • Account Flags: Always true here, but can be customized (e.g., disabled accounts).

PasswordConfig

  • PasswordEncoder Bean:
    • Central place to define how passwords are hashed.
    • Makes it easy to inject into services, repositories, or security configs.
  • BCryptPasswordEncoder:
    • Secure hashing algorithm.
    • Automatically adds salt which prevents rainbow table attacks.
    • Adjustable strength factor to balance security and performance.
  • Integration in JWT Flow:
    • When a user registers: password is hashed using passwordEncoder.encode().
    • Hashed password is stored in the database (UserInfo.password).
    • When user logs in: Spring Security uses passwordEncoder.matches() to verify plain-text password against stored hash.
    • If valid: JWT token is generated and returned.

UserInfoService

  • UserDetailsService:
    • Spring Security uses this interface to load user data during authentication.
    • loadUserByUsername() is called automatically when a user tries to log in.
  • UserInfoService:
    • Implements UserDetailsService.
    • Loads users from DB via UserInfoRepository.
    • Wraps them into UserInfoDetails (adapter for Spring Security).
  • PasswordEncoder (BCrypt):
    • Ensures passwords are stored securely.
    • encoder.encode(): hashes password before saving.
    • encoder.matches(): verifies login attempts.
  • addUser():
    • Handles registration.
    • Encodes password, saves user, and returns success message.

JwtService

  • JWT (JSON Web Token):
    • Compact, secure way to transmit user identity + roles.
    • Contains Header, Payload (claims), Signature.
  • generateToken(): Creates a JWT with subject = username, valid for 30 minutes.
  • extractUsername(): Reads the subject (username) from token.
  • extractClaims(): Generic method that can extract expiration, issuedAt, roles, etc.
  • validateToken(): Ensures token belongs to the right user and is not expired.
  • isTokenExpired(): Checks expiration claim against current time.

JwtAuthFilter

  • OncePerRequestFilter: Ensures JWT validation runs once per request.
  • Authorization Header: Expected format: Authorization: Bearer <token>.
  • jwtService.extractUsername(): Reads username (subject) from token.
  • jwtService.validateToken(): Ensures token is valid and not expired.
  • SecurityContextHolder: Stores authentication info used by Spring Security for authorization.
  • UsernamePasswordAuthenticationToken: Represents authenticated user with roles.

SecurityConfig

  • SecurityFilterChain: Defines security rules for HTTP requests.
  • JWT Filter (JwtAuthFilter): Validates JWT tokens before requests reach controllers.
  • SessionCreationPolicy.STATELESS: No sessions; every request must include JWT.
  • AuthenticationProvider: Uses UserDetailsService + PasswordEncoder to authenticate users.
  • AuthenticationManager: Central authentication engine used when generating tokens.

UserController

  • Public Endpoint: /auth/welcome accessible without login.
  • Registration: /auth/addNewUser saves new users with encoded passwords.
  • Login & Token Generation: /auth/generateToken authenticates credentials and returns JWT.
  • Role-Based Authorization:
    • /auth/user/userProfile: requires ROLE_USER.
    • /auth/admin/adminProfile: requires ROLE_ADMIN.
  • @PreAuthorize: Ensures only users with correct roles (from JWT claims) can access endpoints.

SpringSecurityJwtAuthenticationApplication

  • @SpringBootApplication: Marks this as the main Spring Boot app and auto-configures everything.
  • SpringApplication.run(): Starts the app, loads beans, controllers, filters, and security configuration.
  • Integration:
    • Loads your SecurityConfig (JWT filter + role-based rules).
    • Loads your UserController (registration, login, secured endpoints).
    • Connects UserInfoService and JwtService for authentication and token management.

Flow when /auth/generateToken endpoint with json username & password hit

The /generateToken endpoint is the gateway to your secured application. When you send a JSON request with your username and password, Spring Security orchestrates a sophisticated dance between multiple components to verify your identity and issue a cryptographically signed token.

Here is the complete step-by-step flow of how that process works.


1. The Authentication Flow Diagram

img.png

2. Detailed Step-by-Step Execution

Step A: The Entry Point

  1. Postman sends a POST request to http://localhost:8080/auth/generateToken with a JSON body containing username and password.
  2. UserController receives the request in the authenticateAndGetToken(@RequestBody AuthRequest authRequest) method.
  3. Inside this method, an unauthenticated UsernamePasswordAuthenticationToken object is created using the credentials from the JSON.

Step B: Verification (The Heavy Lifting)

  1. The controller calls authenticationManager.authenticate().
  2. AuthenticationManager delegates this task to the DaoAuthenticationProvider (configured in your SecurityConfig).
  3. The DaoAuthenticationProvider calls UserInfoService.loadUserByUsername().
  • This method calls UserInfoRepository.findByEmail() to fetch the user from your MySQL database.
  • The data is wrapped into a UserInfoDetails object (which implements UserDetails).
  1. The DaoAuthenticationProvider then uses your BCryptPasswordEncoder (from PasswordConfig) to compare the raw password from Postman with the hashed password from the database.

Step C: Success & Token Generation

  1. If the password matches, the AuthenticationManager returns a fully authenticated Authentication object back to the UserController.
  2. The controller checks authentication.isAuthenticated(). If true, it calls jwtService.generateToken(username).
  3. JwtService uses the JJWT library to:
  • Create a set of Claims.
  • Set the expiration time (30 minutes in your code).
  • Sign the token using your SECRET key via getSignKey().
  1. The final Base64-encoded JWT string is returned to the controller.

Step D: The Response

  1. The UserController sends the JWT string back to Postman as a plain text response.

3. Class and Method Call Sequence

Order Class Method Responsibility
1 UserController authenticateAndGetToken() Entry point; starts the process.
2 AuthenticationManager authenticate() Orchestrates the auth check.
3 DaoAuthenticationProvider authenticate() Compares password & user info.
4 UserInfoService loadUserByUsername() Fetches user from DB.
5 UserInfoRepository findByEmail() Executes the SQL query.
6 BCryptPasswordEncoder matches() Checks if the password is valid.
7 JwtService generateToken() Builds and signs the JWT.

4. Key Component Roles

  • JwtAuthFilter: Even though this endpoint is permitAll(), the request still passes through this filter. Since there is no "Bearer" token in the header yet, it simply calls filterChain.doFilter() and moves on.
  • UserInfoDetails: This is the bridge between your JPA Entity (UserInfo) and what Spring Security expects (UserDetails). It handles the logic of converting your comma-separated roles into a List<GrantedAuthority>.
  • SecurityConfig: This class defines that /auth/generateToken does NOT require a token to be accessed (via .permitAll()), allowing users to log in for the first time.

Flow when /auth/admin/adminProfile endpoint with bearer header with <token> hit

Once you have your JWT from the /generateToken endpoint, you use it to access protected resources like /admin/adminProfile. This flow is different because it relies on the JwtAuthFilter to validate the token rather than checking a password against the database.

Here is the step-by-step "Authorization Flow" for a secured request.


1. The Authorization Flow Diagram

img_1.png

2. Detailed Step-by-Step Execution

Step A: The Interception

  1. Postman sends a GET request to http://localhost:8080/auth/admin/adminProfile.
  2. Crucially, you must include the header: Authorization: Bearer <your_token_here>.
  3. The request hits the JwtAuthFilter (your OncePerRequestFilter).

Step B: Token Extraction & Verification

  1. JwtAuthFilter.doFilterInternal() runs:
  • It pulls the Authorization header and checks if it starts with "Bearer ".
  • It extracts the token and calls jwtService.extractUsername(token).
  • It checks the SecurityContextHolder to see if the user is already authenticated (at this point, it is empty/null).
  1. If a username exists and the context is empty, it calls UserInfoService.loadUserByUsername() to get the user's latest roles and details from the database.
  2. The filter calls jwtService.validateToken(token, userDetails):
  • It checks if the username in the token matches the DB.
  • It checks isTokenExpired() by looking at the expiration claim inside the JWT.

Step C: Setting the Security Context

  1. If the token is valid, the filter creates a UsernamePasswordAuthenticationToken.
  • Crucial: It includes the authorities (roles like ROLE_ADMIN) from the userDetails.
  1. It calls SecurityContextHolder.getContext().setAuthentication(authToken).
  • Analogy: This is like the security guard at a building checking your ID and then handing you a "Visitor Badge" that says where you are allowed to go.
  1. The filter calls filterChain.doFilter(), passing the request to the next step.

Step D: Role Checking (Authorization)

  1. The request reaches the AuthorizationFilter in Spring's internal chain.
  2. It looks at the @PreAuthorize("hasAuthority('ROLE_ADMIN')") on your adminProfile method.
  3. It checks the SecurityContextHolder we just filled. If the context contains ROLE_ADMIN, the method executes. If it only contains ROLE_USER, you get a 403 Forbidden.

3. Class and Method Call Sequence

Order Class Method Responsibility
1 JwtAuthFilter doFilterInternal() Intercepts the request and reads the Header.
2 JwtService extractUsername() Decodes the JWT to find out "Who is this?".
3 UserInfoService loadUserByUsername() Fetches the user's roles from the DB.
4 JwtService validateToken() Checks expiration and integrity.
5 SecurityContext setAuthentication() Stores the user's "Badge" for this specific request.
6 UserController adminProfile() The method finally runs and returns the "Welcome" string.

4. Key Takeaway: Statelessness

Notice that in your SecurityConfig, you set SessionCreationPolicy.STATELESS.

This means Spring does not create an HTTP Session or store your login on the server. Every single time you call the /adminProfile endpoint, this entire process (extracting, validating, and fetching from DB) happens again. This is why JWT is perfect for scaling microservices—the server doesn't have to "remember" who you are; the token carries all the proof.


How to Test (Step-by-Step)

Step 1: Register a User

Send a POST request to http://localhost:8080/auth/addNewUser:

{
  "name": "John Doe",
  "email": "john@example.com",
  "password": "password123",
  "roles": "ROLE_USER"
}

Step 2: Generate the Token

Send a POST request to http://localhost:8080/auth/generateToken:

{
"username": "john@example.com",
"password": "password123"
}

Copy the long string returned as the response.

Step 3: Access Protected API

Try to access GET http://localhost:8080/auth/user/userProfile. It will fail with 403 Forbidden initially. Now, add the Authorization header:

Key: Authorization

Value: Bearer <paste_your_token_here>

Result: "Welcome to the User Profile! Your JWT is valid."

About

Full implementation of JWT authentication using Spring Security 6, JJWT library, and MySQL. Features custom filters, stateless session management, and role-based access control. In branch feature/refresh-token have refresh token for long lived token with access token short-lived , automatically delete expired token and logout feature

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages