All Posts
Spring Boot✦ Featured

JWT Auth in Spring Boot — The Right Way

Stop copying StackOverflow. Here's production-grade JWT with refresh tokens, Spring Security filters, and proper error handling.

R
by Rupa
Jan 15, 20252 min readSource Code

Why Most JWT Tutorials Are Wrong

Every JWT tutorial I found did one of these things wrong:

  1. Stored refresh tokens in localStorage (huge security risk)
  2. Never implemented token rotation
  3. Didn't handle token expiry gracefully on the frontend

Let's fix all three.

The Architecture

  • Access token: Short-lived (15 min), stored in memory
  • Refresh token: Long-lived (7 days), stored in HttpOnly cookie
  • Rotation: Refresh tokens rotate on every use

Setup

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.12.3</version>
</dependency>

The JWT Service

JwtService.java
Loading editor...
Secret key length

Your JWT secret must be at least 256 bits (32 bytes) for HS256. Use openssl rand -base64 64 to generate one.

Spring Security Filter

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
  private final JwtService jwtService;
  private final UserDetailsService userDetailsService;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    final String authHeader = request.getHeader("Authorization");
    if (authHeader == null || !authHeader.startsWith("Bearer ")) {
      filterChain.doFilter(request, response);
      return;
    }
    final String jwt = authHeader.substring(7);
    final String username = jwtService.extractUsername(jwt);
    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
      UserDetails userDetails = userDetailsService.loadUserByUsername(username);
      if (jwtService.isTokenValid(jwt, userDetails)) {
        var authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authToken);
      }
    }
    filterChain.doFilter(request, response);
  }
}

Step-by-Step: Refresh Token Flow

Client sends expired access token

The frontend catches a 401 response and automatically calls /auth/refresh.

Server reads HttpOnly cookie

The refresh token comes from the cookie — the client JavaScript can't even read it.

Rotate and respond

We invalidate the old refresh token and issue a new one. This prevents token reuse attacks.

Testing It

test-auth.js
Loading editor...
Production checklist

Before deploying: rotate secrets, enable HTTPS only for cookies, set SameSite=Strict, and log all auth failures.

What's Next?

In the next post we'll add role-based access control (RBAC) and per-endpoint permissions using Spring Security's @PreAuthorize.

#spring-boot#java#jwt#security
⬡ View source code on GitHub →

✦ Enjoyed this post?

Get posts like this in your inbox

No spam, just real tutorials when they're ready.

Discussion

Powered by GitHub

Comments use GitHub Discussions — no separate account needed if you have GitHub.