Back to Security in Microservices
Q3Question 3 of 4

What security features have you implemented, and explain a JWT + OAuth2 implementation end-to-end

Key Concepts
  • JwtUtil creates and verifies tokens: builds claims, signs with a key, and exposes extractUsername and isTokenValid helpers.
  • The authentication filter extends OncePerRequestFilter, reads the Bearer token, validates it, and sets the SecurityContext.
  • The security config declares stateless sessions, public vs protected routes, disables CSRF, and wires the JWT filter before UsernamePasswordAuthenticationFilter.
  • The login endpoint verifies credentials via AuthenticationManager and returns a minted JWT on success.
  • Passwords are hashed with BCrypt so raw passwords are never stored.
  • Short-lived access tokens with refresh-token rotation limit the blast radius of a stolen token.
  • Validate issuer and audience, not just signature; never put sensitive data in the payload.

Definition

The login endpoint verifies credentials and JwtUtil signs a token, a once-per-request filter validates it and sets the security context, and the security config wires stateless sessions with the filter, giving stateless OAuth2-style authentication.
  • 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 SecurityFilterChain bean: 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; PasswordEncoder bean 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.

JAVA
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");
    }
}
TEXT
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.

JAVA
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);
    }
}
TEXT
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.

JAVA
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);
    }
}
TEXT
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.

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

JAVA
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) {}
}
TEXT
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.

TEXT
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?

JAVA
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");
    }
}
TEXT
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.

TEXT
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

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

---

Share
Use ← → arrow keys to navigate