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.
SecurityContextHolderstores 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