Skip to content

Instantly share code, notes, and snippets.

@TCotton
Created February 12, 2026 15:09
Show Gist options
  • Select an option

  • Save TCotton/cf50fe9acc52a0826d2560a832fbc4f8 to your computer and use it in GitHub Desktop.

Select an option

Save TCotton/cf50fe9acc52a0826d2560a832fbc4f8 to your computer and use it in GitHub Desktop.

JWT Refresh Token Strategy: Solving Password Reset Token Invalidation

Problem Statement

The Security Vulnerability

When using JWTs with long expiration times (e.g., 24 hours), a critical security gap emerges during password reset flows:

  1. User authenticates and receives a JWT valid for 24 hours
  2. User initiates a password reset (possibly because their account was compromised)
  3. User successfully resets their password
  4. Security gap: The old JWT remains valid for up to 24 hours
  5. An attacker who previously obtained the JWT can still access the account

Why This Is Dangerous

  • Compromised Credentials: If a user resets their password because they suspect unauthorized access, the old JWT undermines the security action
  • No Immediate Revocation: Stateless JWTs cannot be invalidated without additional infrastructure
  • Extended Attack Window: 24-hour tokens give attackers a full day to exploit stolen tokens
  • False Security: Users believe changing their password protects them immediately

Proposed Solution: Short-Lived Access + Long-Lived Refresh Tokens

Architecture Overview

Replace single long-lived JWTs with a dual-token system:

  1. Access Token (JWT, stateless)

    • Short lifespan: 15 minutes
    • Contains user identity and permissions
    • Used for API authorization
    • Cannot be revoked (stateless)
  2. Refresh Token (opaque, stateful)

    • Long lifespan: 7-30 days
    • Stored in secure database with user association
    • Used only to obtain new access tokens
    • Can be revoked immediately

How It Solves The Problem

Before Password Reset:

  • User has valid access token (expires in ≤15 min)
  • User has valid refresh token (stored in database)

After Password Reset:

  1. Backend deletes all refresh tokens for that user from database
  2. Old access token remains valid for ≤15 minutes (acceptable risk window)
  3. When access token expires, attacker attempts to use refresh token
  4. Backend rejects refresh token (deleted from database)
  5. Attacker is permanently locked out

Maximum exposure window reduced from 24 hours to 15 minutes — a 96% reduction in risk.

Implementation Strategy

Database Schema

refresh_tokens table:

- id: UUID (primary key)
- user_id: UUID (foreign key to users)
- token_hash: string (hashed refresh token)
- expires_at: timestamp
- created_at: timestamp
- last_used_at: timestamp (optional, for auditing)
- device_info: string (optional, for user visibility)
- ip_address: string (optional, for security monitoring)

Token Flow

1. Initial Authentication (Login)

User credentials → Backend
  ↓
Backend validates credentials
  ↓
Generate access token (JWT, 15 min expiry)
  ↓
Generate refresh token (random string, 30 days expiry)
  ↓
Hash refresh token, store in database
  ↓
Return BOTH tokens to client

2. API Request Authorization

Client sends access token in Authorization header
  ↓
Backend validates JWT signature + expiry
  ↓
If valid: Process request
If expired: Return 401 with "token_expired" error

3. Token Refresh

Client sends refresh token to /auth/refresh endpoint
  ↓
Backend hashes token, queries database
  ↓
If found AND not expired:
  - Generate new access token (JWT, 15 min)
  - Optionally rotate refresh token (see rotation strategy)
  - Return new token(s)
If not found OR expired:
  - Return 401, force re-login

4. Password Reset Invalidation

User resets password
  ↓
Backend updates password hash
  ↓
DELETE FROM refresh_tokens WHERE user_id = ?
  ↓
All refresh tokens invalidated
  ↓
Old access tokens expire naturally within 15 min

Security Considerations

Refresh Token Storage

Client-Side:

  • DO: Store in httpOnly, secure, SameSite cookies (web apps)
  • DO: Use secure storage APIs (iOS Keychain, Android Keystore for mobile)
  • DON'T: Store in localStorage (vulnerable to XSS)
  • DON'T: Store in sessionStorage (same XSS risk)

Server-Side:

  • Store only cryptographic hash (e.g., SHA-256 or bcrypt)
  • Never store plaintext refresh tokens
  • Index by user_id for efficient bulk invalidation

Refresh Token Rotation

Strategy: Issue a new refresh token with each refresh request

Benefits:

  • Limits impact of stolen refresh tokens
  • Enables detection of token theft (same token used twice)
  • Provides audit trail

Implementation:

/auth/refresh receives refresh token
  ↓
Validate and consume token (mark as used or delete)
  ↓
Generate NEW refresh token
  ↓
Return new access token + new refresh token

Theft Detection: If a consumed refresh token is used again:

  1. Invalidate all tokens for that user (security breach detected)
  2. Force re-authentication
  3. Alert user of suspicious activity

Cross-Cutting Security Actions

All refresh tokens should be invalidated on:

  • Password reset
  • Password change
  • Email change (if email is authentication identifier)
  • User-initiated "log out all devices"
  • Account deletion
  • Admin-initiated account suspension
  • Security role/permission changes (optional, based on security requirements)

Frontend Integration

Token Management Flow

1. Store both tokens after login
2. Use access token for all API requests
3. On 401 with "token_expired":
   a. Attempt refresh with refresh token
   b. If refresh succeeds: Retry original request with new access token
   c. If refresh fails: Redirect to login
4. Implement automatic refresh before expiry (proactive refresh at 80% lifetime)

Axios Interceptor Pattern (Conceptual)

Response Interceptor:

  • Detect 401 errors with expired token
  • Attempt token refresh
  • Retry failed request with new token
  • On refresh failure, clear tokens and redirect to login

Request Interceptor:

  • Check access token expiry before request
  • If expiring soon (e.g., <2 min remaining), refresh proactively
  • Prevents mid-request token expiration

Database Maintenance

Automatic Cleanup

Expired Token Removal:

  • Schedule daily cleanup job
  • Delete refresh tokens where expires_at < NOW()
  • Prevents database bloat

Inactive Token Pruning:

  • Optional: Remove tokens unused for extended period (e.g., 90 days)
  • Helps identify abandoned devices/sessions

User Session Management

User Dashboard Feature:

  • List active refresh tokens (by device/location)
  • Allow users to revoke individual tokens
  • Provide "log out all devices" action

Trade-offs and Considerations

Advantages

Security: Immediate revocation capability for critical actions
Granular Control: Invalidate specific devices/sessions
Audit Trail: Track token usage patterns
User Transparency: Users can see active sessions
Reduced Risk Window: 15 min vs 24 hours (96% reduction)

Disadvantages

Complexity: More complex than single-token approach
Database Dependency: Refresh endpoint requires database lookup
Storage Overhead: Must store refresh tokens per user/device
Network Overhead: Additional refresh requests
State Management: No longer purely stateless

Performance Optimization

Caching Strategy:

  • Cache refresh token hashes in Redis
  • TTL matches token expiry
  • On invalidation (password reset), clear cache + database
  • Reduces database load for high-traffic apps

Database Indexing:

INDEX on (user_id, expires_at) - for cleanup queries
INDEX on (token_hash) - for validation queries
INDEX on (user_id, created_at) - for session listing

Alternative Solutions (Rejected)

1. Token Blacklist (Not Recommended)

Approach: Maintain database of revoked tokens

Why Rejected:

  • Must store every issued token (enormous database)
  • Every API request requires database lookup (performance hit)
  • Loses stateless JWT benefit entirely
  • Database grows linearly with token issuance rate

2. Very Short Single Token (Not Recommended)

Approach: Use 15-min JWT without refresh tokens

Why Rejected:

  • Forces re-login every 15 minutes (terrible UX)
  • Mobile apps would require constant re-authentication
  • Users wouldn't stay logged in during active sessions

3. Stateful Sessions (Not Recommended for Modern Apps)

Approach: Traditional server-side sessions

Why Rejected:

  • Difficult to scale horizontally
  • No native mobile/SPA support
  • Sticky sessions required for load balancing
  • Doesn't leverage JWT benefits (payload claims, offline validation)

Migration Path

Phase 1: Implement Refresh Token Infrastructure

  • Create refresh_tokens table
  • Build /auth/refresh endpoint
  • Implement token rotation logic

Phase 2: Update Authentication Flow

  • Modify login to return both tokens
  • Update client to store both tokens
  • Maintain backward compatibility (still issue long-lived JWT)

Phase 3: Client Migration

  • Roll out client updates with automatic refresh
  • Monitor error rates
  • Gradual rollout by user segment

Phase 4: Reduce Access Token Lifetime

  • Gradually reduce from 24h → 4h → 1h → 15min
  • Monitor for refresh failures
  • Provide user communication about "more secure sessions"

Phase 5: Enforce Short Tokens

  • Remove long-lived token support
  • All new tokens are 15-min + refresh

Testing Checklist

Functional Tests

  • Login returns both tokens with correct expiry
  • Access token authorizes requests before expiry
  • Expired access token returns 401
  • Refresh token generates new access token
  • Password reset invalidates all refresh tokens
  • Refresh with invalidated token fails
  • Token rotation generates new refresh token
  • Concurrent refresh requests handled correctly

Security Tests

  • Refresh tokens stored as hashes only
  • httpOnly cookies prevent XSS access
  • Refresh token reuse detected and blocked
  • Stolen refresh token cannot be used after password reset
  • Old access token becomes useless within 15 min of password reset

Performance Tests

  • Token refresh completes under 100ms
  • Bulk token invalidation (1000 tokens) completes under 1s
  • Cleanup job handles millions of expired tokens
  • Concurrent refresh requests don't cause race conditions

Monitoring and Observability

Key Metrics

Security Metrics:

  • Refresh token reuse attempts (potential theft indicator)
  • Failed refresh attempts per user (account takeover attempts)
  • Bulk invalidation events (password resets, security actions)

Performance Metrics:

  • Refresh endpoint latency (p50, p95, p99)
  • Database query performance for token validation
  • Cache hit rate (if using Redis)

Usage Metrics:

  • Active refresh tokens per user (device proliferation)
  • Token refresh rate (indicates user activity patterns)
  • Average token lifetime before invalidation

Alerting Thresholds

Critical Alerts:

  • Refresh token reuse spike (>10 incidents/min) → Potential breach
  • Refresh endpoint error rate >5% → System degradation
  • Database connection failures → Service outage

Warning Alerts:

  • Average tokens per user >10 → Possible abandoned sessions
  • Token validation query latency >200ms → Performance degradation

Conclusion

The short-lived access token + long-lived refresh token architecture provides the optimal balance between security, user experience, and system performance for modern authentication systems.

Key Takeaway: By reducing the validity window from 24 hours to 15 minutes and enabling immediate refresh token revocation, we've reduced the attack surface by 96% while maintaining seamless user experience through automatic token refresh.

This solution is industry-standard, used by major platforms (Google, GitHub, Auth0, etc.), and aligns with OAuth 2.0 best practices.


Document Version: 1.0
Last Updated: February 12, 2026
Status: Approved for Implementation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment