Implementing JWT Refresh Tokens in Spring Boot Applications

Securing stateless REST APIs relies heavily on short-lived access tokens paired with longer-lived refresh credentials. This pattern prevents frequent re-authentication while limiting exposure from compromised tokens. Below is a practical implementation of this mechanism using Spring Boot, Spring Security, and the JJWT library.

Core Dependencies

Add the required starters and authentication libraries to your build configuration:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

Token Management Service

Create a centralized utility to handle cryptographic key generation, token creation, parsing, and validation. Separating these concerns improves maintainability and simplifies testing.

package com.secure.api.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;

@Component
public class JwtService {
    private final SecretKey signingKey;
    private final long expirationMs;

    public JwtService() {
        // In production, load keys from environment variables or a secrets manager
        this.signingKey = Keys.hmacShaKeyFor("SecurePrivateKeyStringThatIsLongEnoughForHmacShaxxx".getBytes(StandardCharsets.UTF_8));
        this.expirationMs = 1000L * 60 * 60; // 1 hour
    }

    public String generateToken(String identifier) {
        return Jwts.builder()
                .setSubject(identifier)
                .setIssuedAt(new Date())
                .signWith(signingKey, SignatureAlgorithm.HS256)
                .compact();
    }

    public Claims parseClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(signingKey)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    public boolean isValid(String token) {
        try {
            parseClaims(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    public long getExpirationMs() { return expirationMs; }
}

Security Configuration & Filter

Intercept incoming HTTP requests, extract the bearer token, and populate the security context upon successful validation.

package com.secure.api.config;

import com.secure.api.jwt.JwtService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final JwtService jwtService;

    public SecurityConfig(JwtService jwtService) {
        this.jwtService = jwtService;
    }

    @Bean
    public OncePerRequestFilter jwtAuthFilter() {
        return new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
                String header = request.getHeader("Authorization");
                if (header != null && header.startsWith("Bearer ")) {
                    String token = header.substring(7);
                    if (jwtService.isValid(token)) {
                        Claims claims = jwtService.parseClaims(token);
                        UsernamePasswordAuthenticationToken auth = 
                            new UsernamePasswordAuthenticationToken(claims.getSubject(), null, List.of());
                        SecurityContextHolder.getContext().setAuthentication(auth);
                    }
                }
                chain.doFilter(request, response);
            }
        };
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

Authentication Endpoint

Handle credential verification and issue an initial access token.

package com.secure.api.controller;

import com.secure.api.dto.AuthRequest;
import com.secure.api.dto.TokenResponse;
import com.secure.api.jwt.JwtService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {
    private final AuthenticationManager authManager;
    private final JwtService jwtService;

    public LoginController(AuthenticationManager authManager, JwtService jwtService) {
        this.authManager = authManager;
        this.jwtService = jwtService;
    }

    @PostMapping("/api/auth/login")
    public ResponseEntity<TokenResponse> authenticate(@RequestBody AuthRequest req) {
        authManager.authenticate(new UsernamePasswordAuthenticationToken(req.username(), req.password()));
        String accessToken = jwtService.generateToken(req.username());
        return ResponseEntity.ok(new TokenResponse(accessToken));
    }
}

Refresh Logic Implementation

The core requirement involves validating the existing token, extracting the principal, and issuing a freshly stamped credential. Rather than mutating claim timestamps inline—which can interfere with signature verification—reconstructing the token ensures crytpographic integrity.

package com.secure.api.service;

import com.secure.api.jwt.JwtService;
import org.springframework.stereotype.Service;

@Service
public class TokenRefreshService {
    private final JwtService jwtService;

    public TokenRefreshService(JwtService jwtService) {
        this.jwtService = jwtService;
    }

    public String executeRefresh(String currentToken) {
        var claims = jwtService.parseClaims(currentToken);
        String principal = claims.getSubject();
        return jwtService.generateToken(principal);
    }
}

Refresh Controller & Data Transfer Objects

Expose the renewal endpoint alongside simplified request/response payloads.

package com.secure.api.controller;

import com.secure.api.dto.TokenRequest;
import com.secure.api.dto.TokenResponse;
import com.secure.api.service.TokenRefreshService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RenewalController {
    private final TokenRefreshService refreshService;

    public RenewalController(TokenRefreshService refreshService) {
        this.refreshService = refreshService;
    }

    @PostMapping("/api/auth/renew")
    public ResponseEntity<TokenResponse> renewToken(@RequestBody TokenRequest request) {
        String newToken = refreshService.executeRefresh(request.token());
        return ResponseEntity.ok(new TokenResponse(newToken));
    }
}

Data transfer objects encapsulate communication contracts:

package com.secure.api.dto;

public record AuthRequest(String username, String password) {}
public record TokenResponse(String accessToken) {}
public record TokenRequest(String token) {}

Integration Flow

Upon client login, the /api/auth/login endpoint returns the initial accessToken. The client stores this locally and includes it in the Authorization header for subsequent requests. When downstream services respond with a 401 Unauthorized, the frontend automatically triggers a call to /api/auth/renew using the stale token. The server validates the signature via JwtService.isValid(), extracts the principal identity, generates a new token with a refreshed expiration window, and returns it. The authentication cycle continues until the refresh credential itself fails validation, at which point the user must re-enter credentials. Production deployments should wrap the validation layer in comprehensive exception handling to manage tampered payloads and misconfigured signatures gracefully.

Tags: Spring Boot java JWT Web Security Token Refresh

Posted on Sat, 09 May 2026 22:48:40 +0000 by themire