DevNotes
Support
Spring Security & JWT · Chapter 04

JWT Authentication in Spring Boot

Sessions vs JWT, the login and request flow end to end, a custom OncePerRequestFilter, wiring it into a Spring Security 6 SecurityFilterChain, role-based access with @PreAuthorize, refresh tokens, and how to explain it in an interview.

◷ 12 min read§ 15 sections? 11 interview questions{ } 7 code examples

#Introduction

A typical resume line for a backend developer reads:

"Engineered secure REST APIs with JWT-based authentication and custom filters for token-based request validation and access control."

It means you designed and built APIs where only authenticated and authorized users can reach protected resources. This chapter breaks that sentence down and shows the complete implementation.

PhraseWhat it means
Engineered secure REST APIsBuilt RESTful services with Spring Boot and protected their endpoints from unauthorized access
JWT-based authenticationAfter a successful login, the server issues a JWT containing the username, roles and expiry; the client sends it on every request
Custom filters for token-based request validationA filter (usually extending OncePerRequestFilter) intercepts every request, validates the token and sets the authenticated user
Access controlEndpoints restricted by role, e.g. @PreAuthorize("hasRole('ADMIN')")

#Why JWT Instead of Sessions?

Traditional Session Authentication

User logs in
Server creates a session and stores it
Session ID sent to the browser in a cookie
Every request carries the session ID
Server looks the session up in its session store

Problems at scale: session storage is needed on the server, it's harder to scale across many servers (sticky sessions or a shared store like Redis), and there are extra lookups on each request.

JWT Authentication

User logs in
Server creates a signed JWT
Client stores the JWT
Every request sends the JWT
Server verifies the signature — no session lookup
Session-basedJWT-based
Stateful — the server stores sessionsStateless — everything needed is in the token
Scaling needs sticky sessions or a shared storeAny instance can verify any token
Easy logout — delete the sessionLogout is harder — the token stays valid until it expires
Session ID is tinyThe token is larger and sent on every request
Natural for server-rendered web appsNatural for REST APIs, mobile apps and microservices

#The Architecture

Client sends a login request
Authentication API validates credentials and generates a JWT
Client stores the token
Client calls APIs with Authorization: Bearer token
JWT filter validates the signature and expiry and extracts the user and roles
Spring Security context holds the authenticated user
Protected REST API runs, subject to role checks

#Step 1 — Login and Token Creation

HTTP
POST /login
Content-Type: application/json

{ "username": "john", "password": "1234" }

Validate the credentials with the AuthenticationManager (which uses UserDetailsService and PasswordEncoder, as in Password Encoding):

Java
Authentication authentication = authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(request.username(), request.password()));

If the username doesn't exist or the password doesn't match, this throws an AuthenticationException (e.g. BadCredentialsException) → 401.

Generate the token with the user's roles as a claim:

Java
String token = Jwts.builder()
        .subject(username)
        .claim("roles", List.of("ROLE_ADMIN"))
        .issuedAt(new Date())
        .expiration(new Date(System.currentTimeMillis() + 15 * 60 * 1000))   // 15 minutes
        .signWith(key)
        .compact();

Return it:

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

A Login Controller

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

    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;

    public AuthController(AuthenticationManager authenticationManager, JwtService jwtService) {
        this.authenticationManager = authenticationManager;
        this.jwtService = jwtService;
    }

    @PostMapping("/login")
    public TokenResponse login(@RequestBody LoginRequest request) {
        Authentication auth = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.username(), request.password()));
        UserDetails user = (UserDetails) auth.getPrincipal();
        return new TokenResponse(jwtService.generateToken(user));
    }
}

record LoginRequest(String username, String password) { }
record TokenResponse(String token) { }

#Step 2 — Where the Client Stores the Token

OptionTrade-off
localStorage / sessionStorageEasy, but readable by any JavaScript on the page → vulnerable to XSS
HttpOnly cookieJavaScript can't read it → protects against XSS; but the browser sends it automatically → needs CSRF protection (SameSite, CSRF tokens)
Memory only (a JS variable) + refresh cookieCommon in SPAs: short-lived access token in memory, refresh token in an HttpOnly cookie

Mobile and server-to-server clients simply keep the token in secure storage and send it in a header.

#Step 3 — Sending the Token

HTTP
GET /accounts
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

#Step 4 — The JWT Filter

Every request goes through a custom filter before it reaches the controller:

Java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService 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);    // no token — let Spring Security decide (401 for protected URLs)
            return;
        }

        String token = authHeader.substring(7);
        try {
            String username = jwtService.extractUsername(token);           // verifies signature + expiry

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        } catch (JwtException e) {
            // invalid, tampered or expired token — leave the context empty; protected URLs return 401
        }

        filterChain.doFilter(request, response);
    }
}

What Happens During Validation

1 — extract the token after "Bearer "
2 — verify the signature with the server key
3 — check the expiration (exp)
4 — read the username (sub)
5 — load the user's roles and authorities
6 — create an Authentication object
7 — store it in the SecurityContext
Controller runs with the authenticated user
StepCode / result
Extracttoken = header.substring(7)
Verify signatureRecompute HMACSHA256(header.payload, key) and compare — mismatch → SignatureException
Check expiryExpired → ExpiredJwtException (JJWT checks exp automatically)
Extract usernameclaims.getSubject() → john
Load useruserDetailsService.loadUserByUsername("john") → roles and authorities
Create authenticationnew UsernamePasswordAuthenticationToken(userDetails, null, authorities)
Set contextSecurityContextHolder.getContext().setAuthentication(auth)

Now Spring Security knows: user = john, role = ADMIN.

#Step 5 — Wire the Filter into Spring Security

Without this, the filter never runs in the right place. Spring Security 6 configuration:

Java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity                       // enables @PreAuthorize
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtFilter;

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())                          // OK for header-based tokens; NOT if the JWT is in a cookie
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

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

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
LineWhy
sessionCreationPolicy(STATELESS)Spring Security never creates or uses an HTTP session
addFilterBefore(..., UsernamePasswordAuthenticationFilter.class)The JWT filter runs early in the security filter chain
csrf.disable()Safe only when the token travels in the Authorization header — browsers don't add that header automatically
@EnableMethodSecurityRequired for @PreAuthorize; replaces the deprecated @EnableGlobalMethodSecurity

#Step 6 — Access Control

Java
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public List<User> getUsers() {
    return userService.getUsers();
}
Request reaches /users
Role is ADMIN?
allowed — 200
403 Forbidden

Authentication vs Authorization

AuthenticationAuthorization
Who are you?What can you access?
Username + password, JWT validationADMIN → delete users; USER → view users
Failure → 401 UnauthorizedFailure → 403 Forbidden

#Token Expiration and Refresh Tokens

Java
.expiration(new Date(System.currentTimeMillis() + 3_600_000))   // valid for 1 hour

After the expiry, requests return 401 Unauthorized. Short-lived access tokens are safer, so pair them with a refresh token:

TokenLifetimeUsed for
Access token~15 minutesCalling APIs (Authorization: Bearer)
Refresh token~7 daysOnly to obtain a new access token
Access token expires
Client sends the refresh token to /auth/refresh
Server validates it and issues a new access token
User stays logged in without re-entering the password

#JWT Security Best Practices

  1. Use HTTPS — never send tokens over plain HTTP.
  2. Keep access tokens short-lived — 15–30 minutes.
  3. Store secrets securely — environment variables, Vault or a cloud secret manager, never in source code:
PROPERTIES
jwt.secret=${JWT_SECRET}
  1. Always verify the signature — never trust a payload you've only decoded.
  2. Use refresh tokens instead of long-lived access tokens.
  3. Prefer HttpOnly cookies in browsers to protect against XSS — and add CSRF protection when you do.

#The Interview Answer (3–4 minutes)

JWT is a stateless authentication mechanism commonly used in Spring Boot REST APIs. When a user logs in, Spring Security's AuthenticationManager validates the username and password. If authentication succeeds, the server generates a JWT containing claims such as the username, roles, issued-at time and expiration. The token has a header, payload and signature, and the signature — created with a secret key — makes the token tamper-proof. The client stores the token and sends it as a Bearer token in the Authorization header on every request. A custom filter extending OncePerRequestFilter, registered before UsernamePasswordAuthenticationFilter, extracts the token, validates its signature and expiry, loads the user and sets an Authentication object in the SecurityContext. Spring Security then enforces role-based authorization with rules like hasRole and annotations such as @PreAuthorize. The session policy is stateless, so no session is stored on the server, which makes the design scalable and well suited to microservices.

#Interview Questions

Q1Why JWT instead of session authentication?

It's stateless and needs no server-side session storage, so any instance can verify any request — convenient for REST APIs, mobile clients and microservices. The trade-off is harder revocation.

Q2What information is stored in a JWT?

The subject (username or user ID), roles or authorities, issued-at and expiration times, and any custom claims — never secrets.

Q3What is the structure of a JWT?

Header.Payload.Signature.

Q4Why use a custom filter?

To validate the JWT before the request reaches controllers, and to establish the authenticated user for each request.

Q5What is OncePerRequestFilter?

A Spring base filter class that guarantees the filter logic runs exactly once per request, even when a request is forwarded or dispatched internally.

Q6What happens if the token is expired?

Parsing throws ExpiredJwtException, no authentication is set, and the request to a protected endpoint returns 401 Unauthorized.

Q7What is the difference between 401 and 403?

401 means the user isn't authenticated (no token, invalid or expired token). 403 means they are authenticated but don't have permission.

Q8What is SecurityContextHolder?

It holds the SecurityContext — and therefore the current Authentication — for the current thread. Spring Security reads it to make authorization decisions; the JWT filter populates it.

Q9Why must the filter be added before UsernamePasswordAuthenticationFilter?

So the request is authenticated from the token early in the security filter chain, before Spring Security's authorization checks run.

Q10What is the difference between an access token and a refresh token?

The access token is short-lived and sent with every API call; the refresh token is long-lived, kept more securely, and used only to obtain new access tokens.

Q11Where should a JWT be stored in the browser?

Ideally in an HttpOnly, Secure, SameSite cookie (with CSRF protection) or in memory with a refresh cookie — not in localStorage, which XSS can read.

#Common Mistakes

  • Forgetting addFilterBefore — the filter never authenticates anything.
  • Not making the session policy STATELESS — Spring still creates sessions.
  • hasRole('ADMIN') with authorities stored as ADMIN — always 403.
  • Missing @EnableMethodSecurity — @PreAuthorize is silently ignored.
  • Disabling CSRF while storing the JWT in a cookie.
  • Long-lived access tokens without refresh tokens or revocation.
  • Catching token errors and continuing as if authenticated.

#Key Takeaways

  • Login validates credentials with AuthenticationManager and returns a signed JWT.
  • The client sends Authorization: Bearer <token> on every request.
  • A OncePerRequestFilter verifies the token and puts an Authentication into the SecurityContext.
  • Register the filter with addFilterBefore(..., UsernamePasswordAuthenticationFilter.class) and a STATELESS session policy.
  • @PreAuthorize("hasRole('ADMIN')") needs @EnableMethodSecurity and ROLE_-prefixed authorities.
  • 401 = not authenticated, 403 = not allowed; short access tokens + refresh tokens keep users logged in safely.