Skip to main content

Command Palette

Search for a command to run...

🔄 Deep Dive: Refresh Tokens in Spring Boot Microservices (Java 25 + PostgreSQL + JWT)

Updated
4 min read
P

👨‍💻 I'm Praveen Gupta, a Senior Backend Engineer with over 10+ years of experience in designing scalable backend systems using Java, Spring Boot, Microservices, and AWS Cloud. I specialize in building cloud-native applications, integrating DevOps practices, and mentoring teams in writing clean, production-grade code. I created this blog to share real-world backend architecture tips, Spring Boot patterns, and practical coding solutions that help developers grow faster.

When building secure microservices, access tokens are your first line of defense — but they’re short-lived. What happens when they expire? Do we keep asking users to log in again?
That’s where refresh tokens come in — the silent heroes that keep users authenticated without friction.


🧠 Understanding Access vs Refresh Tokens

Token TypePurposeLifespanStorage
Access TokenGrants access to protected APIsShort (e.g., 15 min)Usually stored in memory or cookies
Refresh TokenGenerates new access tokens without logging in againLong (e.g., 7 days or 30 days)Stored securely (DB or HTTP-only cookie)

⚙️ Token Flow Diagram

🖼️ “JWT Refresh Token Flow in Microservices” simple Flow -


🧩 Token Structure Breakdown

Both Access and Refresh tokens are JWTs (JSON Web Tokens) composed of three parts:
To know what JWT is? Follow the “understanding-jwt-json-web-tokens-the-developer-friendly-guide” link.

Header.Payload.Signature

Example:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwcmF2ZWVuIiwiaWF0IjoxNzM4OTQwMDAsImV4cCI6MTczODk0MDA5fX0.V8fjE2M0T8uCSy8...
PartDescription
HeaderAlgorithm (e.g., HS256)
PayloadUser info (claims) like sub, exp, roles
SignatureHash that ensures data integrity

🏗️ Project Setup (Java 25 + Spring Boot 3.4 + PostgreSQL)

1. Dependencies (Maven)

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

2. application.properties

server.port=8081
spring.datasource.url=jdbc:postgresql://localhost:5432/authdb
spring.datasource.username=postgres
spring.datasource.password=admin
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

jwt.secret=mySuperSecretKey123456
jwt.access.expiration=900000         # 15 min
jwt.refresh.expiration=604800000     # 7 days

🧍‍♂️ User & Token Entities

UserEntity.java

@Entity
@Table(name = "users")
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String password;
    private String role;
}

RefreshTokenEntity.java

@Entity
@Table(name = "refresh_tokens")
public class RefreshTokenEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String token;
    private Instant expiryDate;

    @OneToOne
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    private UserEntity user;
}

🔐 Token Utility Class

JwtUtil.java

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secretKey;

    @Value("${jwt.access.expiration}")
    private long accessTokenExpiry;

    @Value("${jwt.refresh.expiration}")
    private long refreshTokenExpiry;

    public String generateAccessToken(UserEntity user) {
        return Jwts.builder()
                .subject(user.getUsername())
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + accessTokenExpiry))
                .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
                .compact();
    }

    public String generateRefreshToken(UserEntity user) {
        return Jwts.builder()
                .subject(user.getUsername())
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + refreshTokenExpiry))
                .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
                .compact();
    }

    public String extractUsername(String token) {
        return Jwts.parser()
                .verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
                .build()
                .parseSignedClaims(token)
                .getPayload()
                .getSubject();
    }

    public boolean isTokenValid(String token) {
        try {
            Jwts.parser()
                .verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
                .build()
                .parseSignedClaims(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }
}

🔄 Refresh Token Service

RefreshTokenService.java

@Service
public class RefreshTokenService {

    @Autowired
    private RefreshTokenRepository repository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private JwtUtil jwtUtil;

    public RefreshTokenEntity createRefreshToken(String username) {
        UserEntity user = userRepository.findByUsername(username)
                .orElseThrow(() -> new RuntimeException("User not found"));

        RefreshTokenEntity token = new RefreshTokenEntity();
        token.setUser(user);
        token.setExpiryDate(Instant.now().plusMillis(604800000)); // 7 days
        token.setToken(jwtUtil.generateRefreshToken(user));
        return repository.save(token);
    }

    public String refreshAccessToken(String refreshToken) {
        if (!jwtUtil.isTokenValid(refreshToken))
            throw new RuntimeException("Invalid refresh token");

        String username = jwtUtil.extractUsername(refreshToken);
        UserEntity user = userRepository.findByUsername(username)
                .orElseThrow(() -> new RuntimeException("User not found"));

        return jwtUtil.generateAccessToken(user);
    }
}

🚀 Auth Controller

AuthController.java

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

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private RefreshTokenService refreshTokenService;

    @Autowired
    private UserRepository userRepository;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody Map<String, String> loginRequest) {
        String username = loginRequest.get("username");
        String password = loginRequest.get("password");

        UserEntity user = userRepository.findByUsername(username)
                .filter(u -> u.getPassword().equals(password))
                .orElseThrow(() -> new RuntimeException("Invalid credentials"));

        String accessToken = jwtUtil.generateAccessToken(user);
        RefreshTokenEntity refreshToken = refreshTokenService.createRefreshToken(username);

        return ResponseEntity.ok(Map.of(
                "access_token", accessToken,
                "refresh_token", refreshToken.getToken()
        ));
    }

    @PostMapping("/refresh")
    public ResponseEntity<?> refresh(@RequestBody Map<String, String> request) {
        String refreshToken = request.get("refresh_token");
        String newAccessToken = refreshTokenService.refreshAccessToken(refreshToken);

        return ResponseEntity.ok(Map.of("access_token", newAccessToken));
    }
}

🧱 PostgreSQL Schema Snapshot

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(100) UNIQUE NOT NULL,
  password VARCHAR(100) NOT NULL,
  role VARCHAR(50)
);

CREATE TABLE refresh_tokens (
  id SERIAL PRIMARY KEY,
  token TEXT NOT NULL,
  expiry_date TIMESTAMP NOT NULL,
  user_id INT REFERENCES users(id)
);

🧰 Microservice Deployment Design

  • Auth Service → Handles /login and /refresh endpoints

  • Resource Service → Consumes access_token for API access

  • Tokens shared via JWT claims and validated at Gateway


🛡️ Security Best Practices

✅ Use HTTP-only cookies for token storage
✅ Keep refresh tokens short-lived and rotated
✅ Store refresh tokens in PostgreSQL or Redis
✅ Invalidate old tokens during logout
✅ Use TLS (HTTPS) everywhere


🏁 Wrapping Up

Refresh Tokens are essential for a stateless, secure, and user-friendly authentication flow.
By combining JWT, Spring Boot 3.4, and PostgreSQL, you’ve got a production-ready, scalable approach to authentication for microservices.


🚀 Coming Next

In the next post, we’ll integrate this Auth Service with:

  • Spring Cloud Gateway (Java 25) for token validation

  • User Service + Product Service

  • Centralized Config + Docker Compose


💬 Feedback?

Found this useful?
Leave a comment or connect with me on LinkedIn.

You can find the code here:
🔗 GitHub – /blog-refresh-token-java25