Full Stack • Java • System Design • Cloud • AI Engineering

JWT Authentication in Spring Boot

Learn how to implement JWT authentication in Spring Boot step by step using Spring Security, BCrypt, custom JWT filter, login API, token generation, token validation, and protected APIs.

Introduction

JWT stands for JSON Web Token.

JWT is commonly used to secure REST APIs in modern Java and Spring Boot applications.

In traditional web applications, the server stores user session information.

In JWT-based applications, the server does not store session data. Instead, the client sends a token with every request.

This article explains JWT authentication step by step with diagrams and complete Spring Boot implementation.


1. What is JWT?

JWT is a compact token used to securely transfer user identity and claims between client and server.

A JWT usually contains:

  • Header
  • Payload
  • Signature
xxxxx.yyyyy.zzzzz
flowchart LR

JWT["JWT Token"]

JWT --> Header["Header"]
JWT --> Payload["Payload / Claims"]
JWT --> Signature["Signature"]

2. Why JWT?

JWT is useful for stateless REST APIs.

Traditional Session-Based Authentication

sequenceDiagram

participant Client
participant Server
participant SessionStore

Client->>Server: Login username/password
Server->>SessionStore: Create session
Server-->>Client: Session ID cookie
Client->>Server: Request with session cookie
Server->>SessionStore: Validate session
Server-->>Client: Response

Problem:

  • Server stores session
  • Harder to scale across multiple servers
  • Requires session replication or shared session storage

JWT-Based Authentication

sequenceDiagram

participant Client
participant Server

Client->>Server: Login username/password
Server-->>Client: JWT token
Client->>Server: Request with JWT token
Server->>Server: Validate token
Server-->>Client: Response

Benefits:

  • Stateless
  • Scalable
  • Good for REST APIs
  • Works well with microservices
  • Easy to use with mobile and frontend apps

3. JWT Authentication Flow

sequenceDiagram

participant User
participant AuthAPI
participant AuthenticationManager
participant JwtService
participant ProtectedAPI

User->>AuthAPI: Login username/password
AuthAPI->>AuthenticationManager: Authenticate user
AuthenticationManager-->>AuthAPI: Authentication success
AuthAPI->>JwtService: Generate JWT
JwtService-->>AuthAPI: JWT token
AuthAPI-->>User: Return token

User->>ProtectedAPI: Request with Bearer token
ProtectedAPI->>JwtService: Validate token
JwtService-->>ProtectedAPI: Token valid
ProtectedAPI-->>User: Protected response

4. Project Structure

jwt-auth-demo
└── src
    └── main
        └── java
            └── com.codewithvenu.jwt
                ├── config
                │   ├── SecurityConfig.java
                │   └── JwtAuthenticationFilter.java
                ├── controller
                │   ├── AuthController.java
                │   └── DemoController.java
                ├── dto
                │   ├── AuthRequest.java
                │   └── AuthResponse.java
                ├── entity
                │   └── AppUser.java
                ├── repository
                │   └── AppUserRepository.java
                ├── service
                │   ├── JwtService.java
                │   └── CustomUserDetailsService.java
                └── JwtAuthDemoApplication.java

5. Maven Dependencies

<dependencies>

    <!-- REST APIs -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

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

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

    <!-- H2 Database for local testing -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- JWT Library -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.5</version>
    </dependency>

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.12.5</version>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.12.5</version>
        <scope>runtime</scope>
    </dependency>

</dependencies>

6. application.yml

spring:
  datasource:
    url: jdbc:h2:mem:jwtsecuritydb
    driver-class-name: org.h2.Driver
    username: sa
    password:

  h2:
    console:
      enabled: true

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

jwt:
  secret: "my-super-secret-key-my-super-secret-key-my-super-secret-key"
  expiration: 3600000

Explanation

jwt.secret

Used to sign and verify JWT tokens.

jwt.expiration

Token expiry time in milliseconds.

3600000 milliseconds = 1 hour

In production, do not hardcode secret values. Use environment variables or secrets manager.


7. Create User Entity

package com.codewithvenu.jwt.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "app_users")
public class AppUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    @Column(nullable=false)
    private String password;

    @Column(nullable=false)
    private String role;

    protected AppUser() {
    }

    public AppUser(String username, String password, String role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }

    public Long getId() {
        return id;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

    public String getRole() {
        return role;
    }
}

8. Create Repository

package com.codewithvenu.jwt.repository;

import com.codewithvenu.jwt.entity.AppUser;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface AppUserRepository extends JpaRepository<AppUser, Long> {

    Optional<AppUser> findByUsername(String username);
}

This method is used during login and token validation.


9. Create Request and Response DTOs

AuthRequest

package com.codewithvenu.jwt.dto;

public record AuthRequest(
        String username,
        String password
) {
}

AuthResponse

package com.codewithvenu.jwt.dto;

public record AuthResponse(
        String token
) {
}

10. Create CustomUserDetailsService

package com.codewithvenu.jwt.service;

import com.codewithvenu.jwt.entity.AppUser;
import com.codewithvenu.jwt.repository.AppUserRepository;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final AppUserRepository repository;

    public CustomUserDetailsService(AppUserRepository repository) {
        this.repository = repository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) {

        AppUser user = repository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        return User.withUsername(user.getUsername())
                .password(user.getPassword())
                .roles(user.getRole())
                .build();
    }
}

Explanation

UserDetailsService

Spring Security uses this service to load user information.

findByUsername

Finds the user from the database.

roles(user.getRole())

Converts role into Spring Security authority.

Example:

ADMIN → ROLE_ADMIN
USER  → ROLE_USER

11. Create JWT Service

package com.codewithvenu.jwt.service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;

@Service
public class JwtService {

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

    @Value("${jwt.expiration}")
    private long expiration;

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

    public String generateToken(UserDetails userDetails) {

        return Jwts.builder()
                .subject(userDetails.getUsername())
                .claim("roles", userDetails.getAuthorities())
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(getSigningKey())
                .compact();
    }

    public String extractUsername(String token) {

        return extractAllClaims(token)
                .getSubject();
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {

        String username = extractUsername(token);

        return username.equals(userDetails.getUsername())
                && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {

        return extractAllClaims(token)
                .getExpiration()
                .before(new Date());
    }

    private Claims extractAllClaims(String token) {

        return Jwts.parser()
                .verifyWith(getSigningKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }
}

Code Explanation

getSigningKey()

Creates the secret key used to sign and verify JWT.

generateToken()

Creates a JWT token after successful login.

subject(userDetails.getUsername())

Stores username inside token.

issuedAt()

Stores token creation time.

expiration()

Stores token expiry time.

signWith()

Digitally signs the token.

extractUsername()

Reads username from token.

isTokenValid()

Checks username and expiration.


12. JWT Filter

The JWT filter runs once for every request.

It checks whether the request contains a Bearer token.

flowchart TB

Request["HTTP Request"]
--> Filter["JWT Authentication Filter"]

Filter --> TokenCheck{"Bearer Token Present?"}

TokenCheck -- No --> Next["Continue Filter Chain"]

TokenCheck -- Yes --> Validate["Validate Token"]

Validate --> Context["Set SecurityContext"]

Context --> Controller["Controller"]

13. Create JwtAuthenticationFilter

package com.codewithvenu.jwt.config;

import com.codewithvenu.jwt.service.CustomUserDetailsService;
import com.codewithvenu.jwt.service.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final CustomUserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtService jwtService,
                                   CustomUserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain)
            throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");

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

        String token = authHeader.substring(7);

        String username = jwtService.extractUsername(token);

        if (username != null &&
                SecurityContextHolder.getContext().getAuthentication() == null) {

            var userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtService.isTokenValid(token, userDetails)) {

                var authenticationToken =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities()
                        );

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

                SecurityContextHolder.getContext()
                        .setAuthentication(authenticationToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Code Explanation

OncePerRequestFilter

Ensures the filter runs once per request.

Authorization

Reads the Authorization header.

Bearer

JWT tokens are usually sent as:

Authorization: Bearer token-value

substring(7)

Removes "Bearer " prefix.

SecurityContextHolder

Stores authenticated user for the current request.


14. Security Configuration

package com.codewithvenu.jwt.config;

import com.codewithvenu.jwt.service.CustomUserDetailsService;
import org.springframework.context.annotation.*;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.*;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.*;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
            throws Exception {

        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/h2-console/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );

        http.headers(headers -> headers.frameOptions(frame -> frame.disable()));

        http.addFilterBefore(
                jwtAuthenticationFilter,
                UsernamePasswordAuthenticationFilter.class
        );

        return http.build();
    }

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

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

Explanation

csrf.disable()

Disabled for stateless REST API.

/api/auth/**

Login and registration APIs are public.

SessionCreationPolicy.STATELESS

Spring Security will not create HTTP sessions.

addFilterBefore

Adds JWT filter before username/password authentication filter.


15. Create AuthController

package com.codewithvenu.jwt.controller;

import com.codewithvenu.jwt.dto.*;
import com.codewithvenu.jwt.entity.AppUser;
import com.codewithvenu.jwt.repository.AppUserRepository;
import com.codewithvenu.jwt.service.JwtService;
import org.springframework.security.authentication.*;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

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

    private final AuthenticationManager authenticationManager;
    private final AppUserRepository repository;
    private final PasswordEncoder passwordEncoder;
    private final JwtService jwtService;

    public AuthController(AuthenticationManager authenticationManager,
                          AppUserRepository repository,
                          PasswordEncoder passwordEncoder,
                          JwtService jwtService) {
        this.authenticationManager = authenticationManager;
        this.repository = repository;
        this.passwordEncoder = passwordEncoder;
        this.jwtService = jwtService;
    }

    @PostMapping("/register")
    public String register(@RequestBody AuthRequest request) {

        boolean exists = repository.findByUsername(request.username()).isPresent();

        if (exists) {
            return "Username already exists";
        }

        AppUser user = new AppUser(
                request.username(),
                passwordEncoder.encode(request.password()),
                "USER"
        );

        repository.save(user);

        return "User registered successfully";
    }

    @PostMapping("/login")
    public AuthResponse login(@RequestBody AuthRequest request) {

        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        request.username(),
                        request.password()
                )
        );

        AppUser user = repository.findByUsername(request.username())
                .orElseThrow();

        UserDetails userDetails = org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .roles(user.getRole())
                .build();

        String token = jwtService.generateToken(userDetails);

        return new AuthResponse(token);
    }
}

16. Create Protected Controller

package com.codewithvenu.jwt.controller;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class DemoController {

    @GetMapping("/user/profile")
    public String userProfile() {
        return "User profile data";
    }

    @GetMapping("/admin/dashboard")
    public String adminDashboard() {
        return "Admin dashboard data";
    }
}

17. Full JWT Request Flow

sequenceDiagram

participant Client
participant JwtFilter
participant JwtService
participant UserDetailsService
participant SecurityContext
participant Controller

Client->>JwtFilter: Request with Bearer Token
JwtFilter->>JwtService: Extract username
JwtFilter->>UserDetailsService: Load user
JwtFilter->>JwtService: Validate token
JwtFilter->>SecurityContext: Store authentication
JwtFilter->>Controller: Forward request
Controller-->>Client: Response

18. Test Registration

curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{
  "username": "venu",
  "password": "password123"
}'

Expected:

User registered successfully

19. Test Login

curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
  "username": "venu",
  "password": "password123"
}'

Expected:

{
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}

20. Access Protected API

curl http://localhost:8080/api/user/profile \
-H "Authorization: Bearer YOUR_TOKEN_HERE"

Expected:

User profile data

Without token:

curl http://localhost:8080/api/user/profile

Expected:

401 Unauthorized

21. JWT Best Practices

  • Use HTTPS
  • Keep token expiry short
  • Store secrets outside source code
  • Use refresh tokens for long sessions
  • Do not store sensitive data in JWT payload
  • Validate token signature
  • Validate expiration
  • Use strong signing key
  • Rotate signing keys periodically
  • Log failed authentication attempts

22. Common Mistakes

Mistake Problem
Storing password in JWT Severe data leak
Long-lived tokens Higher risk if stolen
Weak secret key Token can be forged
No HTTPS Token can be intercepted
Not validating expiration Expired tokens still work
Saving JWT in logs Token leakage

23. Interview Questions

Q1. What is JWT?

JWT is a JSON Web Token used to transfer identity and claims between client and server.

Q2. Is JWT stateful or stateless?

JWT authentication is stateless.

Q3. Where is JWT sent?

Usually in the Authorization header as a Bearer token.

Q4. What are the parts of JWT?

Header, Payload, and Signature.

Q5. What is the purpose of the signature?

The signature proves the token was not modified.

Q6. Should sensitive data be stored inside JWT?

No. JWT payload can be decoded. Do not store passwords, SSNs, or secrets.

Q7. Why use SessionCreationPolicy.STATELESS?

Because JWT APIs do not use server-side sessions.

Q8. What does JWT filter do?

It extracts token, validates token, loads user, and sets authentication in SecurityContext.


24. Key Takeaways

  • JWT is widely used for stateless API authentication.
  • Client sends JWT with every protected request.
  • Server validates JWT signature and expiration.
  • Spring Security uses a custom filter for JWT validation.
  • SecurityContextHolder stores authenticated user for current request.
  • BCrypt should be used to hash passwords.
  • Do not store sensitive information in JWT.
  • Use HTTPS in production.

Next Article

➡️ Refresh Token Implementation in Java