Mastering Double Token Authentication: Secure & Seamless Access for Web and Mobile Apps

This article explains the double token (Access Token + Refresh Token) authentication model, compares it with single‑token approaches, outlines production‑grade security measures, recommends token lifetimes, and provides a complete Java Spring Boot implementation with code examples.

Ray's Galactic Tech
Ray's Galactic Tech
Ray's Galactic Tech
Mastering Double Token Authentication: Secure & Seamless Access for Web and Mobile Apps

Core Concepts

Access Token

Purpose: Credential for accessing protected resources.

Characteristics:

Short lifespan (10–30 minutes)

Sent with every API request

Low impact if leaked

Typical storage: In‑memory, localStorage, sessionStorage

Refresh Token

Purpose: Obtain a new Access Token.

Characteristics:

Long lifespan (7–30 days)

Never used directly to access resources

Must be stored securely and be revocable

Recommended storage: HttpOnly, Secure cookie

Authentication Workflow

1. Initial Login

Client submits credentials → server validates → returns an Access Token (short‑term) and a Refresh Token (long‑term) stored in an HttpOnly cookie.

2. API Calls

Each request includes Authorization: Bearer <access_token> header.

3. Access Token Expiration

Server responds 401 (token expired).

Client calls /refresh using the Refresh Token from the cookie.

Server issues a new Access Token (and a rotated Refresh Token).

Client retries the original request transparently.

4. Refresh Token Expiration

User must re‑authenticate.

Production‑Grade Security Measures

1. Persistent Refresh Token Store

Store each Refresh Token with metadata (user, IP, device, expiration) in a database or Redis to enable revocation and device management.

refresh_token:{token_id} = {</code><code>    "userId": 1001,</code><code>    "ip": "1.2.3.4",</code><code>    "device": "Chrome XX",</code><code>    "exp": "2025-01-01"</code><code>}

2. Refresh Token Rotation

Generate a new Refresh Token on every successful refresh.

Invalidate the previous token.

If an old token is reused, treat it as a leak and force re‑authentication.

3. Multi‑Device Login Control

Limit the maximum number of concurrent devices per user.

When the limit is exceeded, evict the earliest device.

4. Risk‑Based Controls

Detect sudden IP or geographic changes.

Detect the same Refresh Token used on multiple devices.

Detect rapid, repeated refreshes (possible bots).

Automatically revoke compromised tokens and require login.

Recommended Token Lifetimes

Access Token: 10–30 minutes.

Refresh Token: 7–30 days.

Refresh Token Rotation: New token on every refresh to prevent replay attacks.

Java Spring Boot Implementation

1. JWT Utility

@Component
public class JwtUtil {
    private final String ACCESS_SECRET = "your-access-secret-key";
    private final String REFRESH_SECRET = "your-refresh-secret-key";

    public String generateAccessToken(Long userId) {
        return Jwts.builder()
            .setSubject(String.valueOf(userId))
            .setExpiration(Date.from(Instant.now().plus(Duration.ofMinutes(15))))
            .signWith(SignatureAlgorithm.HS256, ACCESS_SECRET)
            .compact();
    }

    public String generateRefreshToken(Long userId) {
        return Jwts.builder()
            .setSubject(String.valueOf(userId))
            .setExpiration(Date.from(Instant.now().plus(Duration.ofDays(7))))
            .signWith(SignatureAlgorithm.HS256, REFRESH_SECRET)
            .compact();
    }

    public Claims validateAccessToken(String token) { return parse(token, ACCESS_SECRET); }
    public Claims validateRefreshToken(String token) { return parse(token, REFRESH_SECRET); }

    private Claims parse(String token, String secret) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }
}

2. Refresh Token Storage (Redis)

@Service
public class RefreshTokenService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private final String KEY_PREFIX = "refresh_token:";

    public void storeRefreshToken(String token, Long userId) {
        redisTemplate.opsForValue().set(KEY_PREFIX + token, String.valueOf(userId), Duration.ofDays(7));
    }

    public Long getUserId(String token) {
        String value = redisTemplate.opsForValue().get(KEY_PREFIX + token);
        return value == null ? null : Long.valueOf(value);
    }

    public void revoke(String token) {
        redisTemplate.delete(KEY_PREFIX + token);
    }
}

3. Login Endpoint (Issue Double Tokens)

@RestController
@RequestMapping("/auth")
public class AuthController {
    @Autowired private JwtUtil jwtUtil;
    @Autowired private RefreshTokenService refreshTokenService;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest req, HttpServletResponse response) {
        // Validate credentials (omitted)
        Long userId = 1001L;
        String accessToken = jwtUtil.generateAccessToken(userId);
        String refreshToken = jwtUtil.generateRefreshToken(userId);
        refreshTokenService.storeRefreshToken(refreshToken, userId);
        ResponseCookie cookie = ResponseCookie.from("refreshToken", refreshToken)
            .httpOnly(true).secure(true).path("/").maxAge(7 * 24 * 3600).build();
        response.addHeader("Set-Cookie", cookie.toString());
        return ResponseEntity.ok(Map.of("accessToken", accessToken));
    }
}

4. Refresh Endpoint with Rotation

@PostMapping("/refresh")
public ResponseEntity<?> refresh(@CookieValue(value = "refreshToken", required = false) String refreshToken,
                                 HttpServletResponse response) {
    if (refreshToken == null) {
        return ResponseEntity.status(401).body("Refresh Token missing");
    }
    Long userId = refreshTokenService.getUserId(refreshToken);
    if (userId == null) {
        return ResponseEntity.status(401).body("Refresh Token invalid");
    }
    try {
        jwtUtil.validateRefreshToken(refreshToken);
    } catch (Exception e) {
        return ResponseEntity.status(401).body("Refresh Token expired");
    }
    String newAccessToken = jwtUtil.generateAccessToken(userId);
    String newRefreshToken = jwtUtil.generateRefreshToken(userId);
    refreshTokenService.revoke(refreshToken);
    refreshTokenService.storeRefreshToken(newRefreshToken, userId);
    ResponseCookie cookie = ResponseCookie.from("refreshToken", newRefreshToken)
        .httpOnly(true).secure(true).path("/").maxAge(7 * 24 * 3600).build();
    response.addHeader("Set-Cookie", cookie.toString());
    return ResponseEntity.ok(Map.of("accessToken", newAccessToken));
}

5. Access Token Filter

@Component
public class JwtAuthFilter extends OncePerRequestFilter {
    @Autowired private JwtUtil jwtUtil;
    @Autowired private UserDetailsServiceImpl userDetailsService;

    @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);
            try {
                Claims claims = jwtUtil.validateAccessToken(token);
                Long userId = Long.valueOf(claims.getSubject());
                UserDetails userDetails = userDetailsService.loadUserById(userId);
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(auth);
            } catch (Exception e) {
                // Authentication will fail downstream, triggering refresh flow.
            }
        }
        chain.doFilter(request, response);
    }
}

Applicable Scenarios

Single‑Page Applications (React, Vue) requiring long‑lived sessions.

Mobile apps or mini‑programs that need persistent login.

Systems with multi‑device access (e‑commerce, social platforms).

Use cases where sudden disconnection would break user flow (online forms, instant messaging).

Conclusion

The double‑token pattern balances security and usability by using short‑lived Access Tokens for resource protection and long‑lived Refresh Tokens for seamless session renewal. Implementing token rotation, persistent storage, and risk‑based revocation—as demonstrated in the Java Spring Boot code—provides a robust, enterprise‑grade authentication solution.

Double Token Architecture Diagram
Double Token Architecture Diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootJWTaccess tokenRefresh Tokendouble token
Ray's Galactic Tech
Written by

Ray's Galactic Tech

Practice together, never alone. We cover programming languages, development tools, learning methods, and pitfall notes. We simplify complex topics, guiding you from beginner to advanced. Weekly practical content—let's grow together!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.