Definition
- JwtUtil – Creates tokens by building claims (subject, roles, expiry) and signing them, and verifies by parsing and checking signature and expiry.
- Authentication filter – Extends
OncePerRequestFilter; reads the Bearer token, validates via JwtUtil, and populates the SecurityContextHolder. - Security config – A
SecurityFilterChainbean: stateless session, public vs protected routes, CSRF disabled, JWT filter registered. - Login endpoint – Receives username/password, delegates to
AuthenticationManager, and returns a minted JWT on success. - BCrypt – Password hashing so raw passwords are never stored;
PasswordEncoderbean exposed for the auth manager.
Imagine National Bank of India gives each customer a tamper-proof wristband when they prove their identity at the entrance (login endpoint minting JWT). For the rest of their visit, every department scans the wristband to confirm it is genuine and not expired (filter validating token). The bank's rulebook says which areas are open to all and which need a scanned wristband (security config). Because the wristband itself proves who you are, no department keeps a list of who is inside, which is what stateless means.
Understanding the Concepts
What security features do you implement on a banking platform?
A banking platform requires layered security: stateless JWT authentication, RBAC, BCrypt password hashing, HTTPS, CORS, CSRF handling, short-lived tokens, and clean error responses.
package com.bank.security;
public class SecurityFeaturesList {
public static void main(String[] args) {
System.out.println("Security features implemented:");
System.out.println(" 1. Stateless JWT authentication (no server sessions)");
System.out.println(" 2. Role-based authorization (@PreAuthorize)");
System.out.println(" 3. BCrypt password hashing (raw passwords never stored)");
System.out.println(" 4. HTTPS everywhere (TLS in transit)");
System.out.println(" 5. CORS restricted to trusted origins");
System.out.println(" 6. CSRF disabled for stateless token API");
System.out.println(" 7. Short-lived access tokens + refresh tokens");
System.out.println(" 8. Clean 401/403 responses without stack traces");
}
}
Expected output:
Security features implemented:
1. Stateless JWT authentication (no server sessions)
2. Role-based authorization (@PreAuthorize)
3. BCrypt password hashing (raw passwords never stored)
4. HTTPS everywhere (TLS in transit)
5. CORS restricted to trusted origins
6. CSRF disabled for stateless token API
7. Short-lived access tokens + refresh tokens
8. Clean 401/403 responses without stack traces
How does JwtUtil create and verify tokens?
JwtUtil builds claims (subject, roles, expiry), signs with a key, and provides helpers to extract claims and check validity.
package com.bank.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration-ms:300000}") // 5 minutes default
private long expirationMs;
private Key getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
public String generateToken(String username, List<String> roles) {
return Jwts.builder()
.setSubject(username)
.claim("roles", roles)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public boolean isTokenValid(String token, String username) {
return extractUsername(token).equals(username) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration).before(new Date());
}
private <T> T extractClaim(String token, Function<Claims, T> resolver) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return resolver.apply(claims);
}
}
Generated JWT structure:
Header: {"alg":"HS256","typ":"JWT"}
Payload: {"sub":"rajesh","roles":["CUSTOMER"],"iat":1705312200,"exp":1705312500}
Signature: HMAC-SHA256(header.payload, secret)
How does the authentication filter work?
The filter runs once per request, reads the Bearer token, validates it, and populates the SecurityContext.
package com.bank.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
String username = jwtUtil.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
if (jwtUtil.isTokenValid(token, username)) {
var authorities = List.of(new SimpleGrantedAuthority("ROLE_CUSTOMER"));
var authToken = new UsernamePasswordAuthenticationToken(
username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
}
chain.doFilter(request, response);
}
}
Filter flow:
1. Read Authorization header
2. Extract Bearer token
3. Validate signature + expiry via JwtUtil
4. If valid: set authentication in SecurityContext
5. If invalid/missing: no authentication set -> 401 on protected routes
How does the security config wire everything together?
The config declares stateless sessions, public vs protected routes, disables CSRF, and registers the JWT filter.
package com.bank.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;
public SecurityConfig(JwtAuthenticationFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/actuator/health/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
Security config summary:
- CSRF disabled (stateless token API, no cookies)
- Sessions: STATELESS (JWT carries state)
- Public: /auth/**, /actuator/health/**
- Protected: everything else
- JWT filter runs BEFORE UsernamePasswordAuthenticationFilter
- BCrypt for password hashing
How does the login endpoint issue tokens?
The login endpoint receives credentials, verifies via AuthenticationManager, and returns a JWT.
package com.bank.security;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
private final AuthenticationManager authManager;
private final JwtUtil jwtUtil;
public AuthController(AuthenticationManager authManager, JwtUtil jwtUtil) {
this.authManager = authManager;
this.jwtUtil = jwtUtil;
}
@PostMapping("/login")
public AuthResponse login(@RequestBody LoginRequest request) {
authManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.username(), request.password()));
String token = jwtUtil.generateToken(request.username(), List.of("CUSTOMER"));
System.out.println("JWT issued for: " + request.username());
return new AuthResponse(token);
}
record LoginRequest(String username, String password) {}
record AuthResponse(String accessToken) {}
}
Expected output:
JWT issued for: rajesh
Response:
{"accessToken":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyYWplc2gi..."}
What is the complete end-to-end flow?
Login once, get a token, send it on every subsequent request. Each service validates locally.
End-to-end JWT flow:
1. POST /api/v1/auth/login {username: rajesh, password: ****}
2. AuthController verifies credentials via AuthenticationManager
3. JwtUtil.generateToken("rajesh", ["CUSTOMER"]) -> signed JWT
4. Return JWT to client
5. Client stores token
6. GET /api/v1/accounts/SAV2001 Authorization: Bearer <token>
7. JwtAuthenticationFilter validates token, sets SecurityContext
8. @PreAuthorize("hasRole('CUSTOMER')") passes -> account returned
What are the WRONG patterns to avoid?
package com.bank.security;
public class SecurityAntiPatterns {
// WRONG: Storing raw passwords
// userRepository.save(new User("rajesh", "password123"));
// -> ALWAYS use BCrypt: passwordEncoder.encode("password123")
// WRONG: Sensitive data in JWT payload
// claims.put("accountBalance", 950000); // readable by anyone!
// -> JWT payload is only Base64Url-encoded, NOT encrypted
// WRONG: Long-lived tokens with no refresh
// expirationMs = 86400000; // 24 hours
// -> A stolen token is usable for 24 hours
// -> Use 5-minute access tokens + refresh rotation
public static void main(String[] args) {
System.out.println("Security anti-patterns:");
System.out.println(" 1. Raw passwords in DB -> use BCrypt");
System.out.println(" 2. Sensitive data in JWT -> payload is readable");
System.out.println(" 3. Long-lived tokens -> use short access + refresh");
System.out.println(" 4. Not validating issuer/audience -> token replay attack");
}
}
Expected output:
Security anti-patterns:
1. Raw passwords in DB -> use BCrypt
2. Sensitive data in JWT -> payload is readable
3. Long-lived tokens -> use short access + refresh
4. Not validating issuer/audience -> token replay attack
What should a senior engineer know?
Keep tokens short-lived, validate issuer and audience, store signing keys in a vault, prefer RS256 in multi-service systems, and return consistent 401/403.
Senior guidelines:
[x] Access tokens: 5-minute expiry
[x] Refresh tokens: rotate on each use, revoke on logout
[x] Signing key: in Vault, not in application.yml
[x] RS256 preferred: services verify with public key, cannot mint tokens
[x] Validate: iss (issuer), aud (audience), exp (expiry)
[x] 401 for unauthenticated, 403 for unauthorized (consistent)
[x] Never log or echo token values
Visual Overview
JWT + OAuth2 End-to-End Flow:
┌──────────┐ POST /auth/login ┌──────────────────┐
│ Client │────────────────────▶│ AuthController │
│ (mobile) │ {user, password} │ │
└──────────┘ │ AuthManager │
│ │ -> BCrypt verify │
│ │ -> JwtUtil.gen() │
│◀──────────────────────────│ {accessToken} │
│ JWT token └──────────────────┘
│
│ GET /accounts/SAV2001
│ Authorization: Bearer <token>
│
▼
┌──────────────────┐
│ JwtFilter │ 1. Extract Bearer token
│ (OncePerRequest) │ 2. JwtUtil.isTokenValid()
│ │ 3. Set SecurityContext
└────────┬─────────┘
│
┌────────▼─────────┐
│ SecurityConfig │ @PreAuthorize("hasRole('CUSTOMER')")
│ -> AUTHORIZED │
└────────┬─────────┘
│
┌────────▼─────────┐
│ AccountController│ Returns account details
└──────────────────┘
Key Takeaway
A JWT plus OAuth2 implementation in Spring Security has four concrete pieces. JwtUtil creates tokens by building claims and signing them, and verifies by parsing and checking signature and expiry. The JwtAuthenticationFilter extends OncePerRequestFilter to read the Bearer token, validate it, and populate the SecurityContext on every request. The SecurityFilterChain bean declares stateless sessions, permits public endpoints like login, requires authentication for everything else, disables CSRF, and wires the filter before the standard username-password filter. The login endpoint verifies credentials via AuthenticationManager and returns a minted JWT.
At National Bank of India, this let Rajesh log in once and call the account and transfer endpoints with a bearer token that every service trusted on every request, with no server-side session. The filter made validation automatic so no developer could forget it, and the config cleanly separated public routes from protected ones. Passwords are hashed with BCrypt so raw passwords are never stored.
The senior nuances matter in production: keep access tokens short-lived (5 minutes) with refresh-token rotation, validate the token's issuer and audience not just its signature, never put sensitive data in the payload because it is only encoded not encrypted, store the signing key in a vault, and prefer RS256 in multi-service systems so services verify with a public key without holding the signing secret.
Remember one sentence: the login endpoint mints a short-lived JWT, a once-per-request filter validates it and sets the security context, and the security config wires stateless sessions with the filter, giving clean, scalable, stateless authentication.
---