Refresh Token Expiry — Absolute and Sliding Window Expiration Strategies
In this tutorial, you will learn about Refresh Token Expiry. We cover key concepts, practical examples, and best practices to help you master this topic.
Refresh token expiry defines how long a user session can persist without re-authentication, using absolute deadlines (max session length) and sliding Windows (idle timeout) for different security requirements.
What You'll Learn
Absolute vs Sliding Window expiry, configuring refresh token lifetimes per client type, idle timeout detection, session extension policies, and expiry enforcement in Distributed Systems.
Why It Matters
A refresh token that never expires is a permanent backdoor. Proper expiry policies balance security and UX — force re-authentication for sensitive operations while allowing long-lived sessions for trusted internal tools.
Real-World Use
Banking apps use short absolute expiry (15 min idle, 8 hour absolute). GitHub uses sliding expiry (renewed on activity). Durga Antivirus Pro uses sliding expiry for analysts (renewed while active) and absolute expiry for partner integrations (24 hours max).
flowchart TD
A["Refresh Token Issued"] --> B{"Expiry type?"}
B -->|"Absolute"| C["Fixed expiry time\n(e.g., 24 hours)"]
B -->|"Sliding"| D["Expiry extended\non each refresh"]
B -->|"Hybrid"| E["Sliding window within\nmax absolute limit"]
C --> F["Forced re-auth at deadline"]
D --> G{"User active?"}
G -->|"Yes — refresh"| D
G -->|"Idle > limit"| H["Session expires"]
E --> I["Sliding extends\nuntil absolute cap"]
style C fill:#fef3c7,stroke:#d97706
style D fill:#dbeafe,stroke:#2563eb
style E fill:#dcfce7,stroke:#16a34a
style F fill:#fecaca,stroke:#dc2626
Code Example: Hybrid Expiry Implementation
import jwt, datetime, secrets
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET = "expiry-secret"
# Expiry configuration per client type
EXPIRY_CONFIG = {
"web-app": {
"absolute_max_seconds": 86400, # 24 hours
"sliding_window_seconds": 3600, # 1 hour idle
"refresh_token_seconds": 604800 # 7 days rotation
},
"mobile-app": {
"absolute_max_seconds": 2592000, # 30 days
"sliding_window_seconds": 86400, # 24 hours idle
"refresh_token_seconds": 2592000 # 30 days
},
"partner-integration": {
"absolute_max_seconds": 3600, # 1 hour
"sliding_window_seconds": 1800, # 30 min idle
"refresh_token_seconds": 7200 # 2 hours
}
}
def get_expiry_policy(client_type="web-app"):
return EXPIRY_CONFIG.get(client_type, EXPIRY_CONFIG["web-app"])
def issue_refresh_token(user_id, client_type):
policy = get_expiry_policy(client_type)
now = datetime.datetime.utcnow()
refresh_token = secrets.token_urlsafe(32)
token_data = {
"user_id": user_id,
"client_type": client_type,
"absolute_expiry": (now + datetime.timedelta(
seconds=policy["absolute_max_seconds"])
).timestamp(),
"last_activity": now.timestamp(),
"sliding_window_seconds": policy["sliding_window_seconds"],
"token": hashlib.sha256(refresh_token.encode()).hexdigest()
}
# Store in database
refresh_tokens[token_data["token"]] = token_data
return refresh_token, token_data
def validate_refresh_expiry(token_data):
"""Check both absolute and sliding expiry."""
now = datetime.datetime.utcnow().timestamp()
# Check absolute expiry
if now > token_data["absolute_expiry"]:
return False, "Session expired — maximum lifetime reached"
# Check sliding window (idle timeout)
idle_time = now - token_data["last_activity"]
if idle_time > token_data["sliding_window_seconds"]:
return False, "Session expired — idle timeout"
return True, "OK"
@app.route("/api/auth/login", methods=["POST"])
def login():
user = request.json.get("username", "analyst")
client_type = request.headers.get("X-Client-Type", "web-app")
refresh_token, _ = issue_refresh_token(user, client_type)
access_token = jwt.encode({
"sub": user,
"client_type": client_type,
"iat": datetime.datetime.utcnow(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
}, SECRET, algorithm="HS256")
return jsonify({
"access_token": access_token,
"refresh_token": refresh_token,
"expires_in": 900,
"session_policy": get_expiry_policy(client_type)
})
@app.route("/api/auth/refresh", methods=["POST"])
def refresh():
refresh_token = request.json.get("refresh_token")
token_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
stored = refresh_tokens.get(token_hash)
if not stored:
return jsonify({"error": "Invalid refresh token"}), 401
valid, message = validate_refresh_expiry(stored)
if not valid:
del refresh_tokens[token_hash]
return jsonify({"error": message}), 401
# Update last activity
stored["last_activity"] = datetime.datetime.utcnow().timestamp()
new_access = jwt.encode({
"sub": stored["user_id"],
"iat": datetime.datetime.utcnow(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
}, SECRET, algorithm="HS256")
return jsonify({
"access_token": new_access,
"expires_in": 900,
"session_remaining": stored["absolute_expiry"] - datetime.datetime.utcnow().timestamp()
})
Code Example: Idle Timeout Detection
import time
class IdleTimeoutDetector:
"""Detects and handles idle timeout across requests."""
def __init__(self, redis_client):
self.redis = redis_client
def record_activity(self, user_id):
"""Update user's last activity timestamp."""
now = time.time()
self.redis.set(f"user_activity:{user_id}", now)
def check_idle(self, user_id, max_idle_seconds=3600):
"""Check if user has been idle too long."""
last_activity = self.redis.get(f"user_activity:{user_id}")
if not last_activity:
return False
idle_time = time.time() - float(last_activity)
return idle_time > max_idle_seconds
def get_idle_seconds(self, user_id):
"""Get how many seconds the user has been idle."""
last_activity = self.redis.get(f"user_activity:{user_id}")
if not last_activity:
return None
return int(time.time() - float(last_activity))
# Middleware to update activity
@app.before_request
def update_activity():
if hasattr(g, 'current_user') and g.current_user:
user_id = g.current_user.get("sub")
if user_id:
idle_detector.record_activity(user_id)
# Endpoint to check session status
@app.route("/api/auth/session-status")
def session_status():
if not hasattr(g, 'current_user'):
return jsonify({"authenticated": False})
user_id = g.current_user.get("sub")
idle_seconds = idle_detector.get_idle_seconds(user_id)
refresh_token_hash = hashlib.sha256(
request.headers.get("X-Refresh-Token", "").encode()
).hexdigest()
stored = refresh_tokens.get(refresh_token_hash)
status = {
"authenticated": True,
"user": user_id,
"idle_seconds": idle_seconds
}
if stored:
remaining = stored["absolute_expiry"] - time.time()
status["session_remaining_seconds"] = max(0, int(remaining))
status["absolute_expiry"] = stored["absolute_expiry"]
return jsonify(status)
Code Example: Client-Side Expiry-Aware Token Management
class ExpiryAwareClient {
constructor(config) {
this.config = config;
this.accessToken = null;
this.refreshTokenData = null;
this.expiryTimer = null;
}
async login(username, password) {
const resp = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-Type': this.config.clientType
},
body: JSON.stringify({ username, password })
});
const data = await resp.json();
this.accessToken = data.access_token;
this.refreshTokenData = {
token: data.refresh_token,
policy: data.session_policy
};
this.scheduleExpiryCheck(data.session_policy);
return data;
}
scheduleExpiryCheck(policy) {
// Check at 80% of the sliding window
const checkInterval = (policy.sliding_window_seconds * 0.8) * 1000;
if (this.expiryTimer) clearInterval(this.expiryTimer);
this.expiryTimer = setInterval(() => this.checkSession(), checkInterval);
}
async checkSession() {
const resp = await fetch('/api/auth/session-status', {
headers: { 'Authorization': `Bearer ${this.accessToken}` }
});
const status = await resp.json();
if (status.session_remaining_seconds < 300) {
// Less than 5 minutes remaining — warn user
this.emit('session-expiring', status);
}
if (status.idle_seconds && status.idle_seconds > this.config.warnIdleSeconds) {
this.emit('idle-warning', status);
}
}
on(event, handler) {
this.events = this.events || {};
this.events[event] = handler;
}
emit(event, data) {
if (this.events && this.events[event]) {
this.events[event](data);
}
}
}
Common Mistakes
1. No Absolute Expiry
Sliding-only expiry means the user never needs to re-authenticate. This is dangerous for sensitive applications. Always set an absolute maximum session length.
2. Sliding Window Too Long
A 24-hour sliding window means a stolen token works for 24 hours. Balance security needs — 1 hour for high-security, 24 hours for low-risk applications.
3. Not Updating Activity on All Requests
If activity is only updated on explicit refresh endpoints, the idle timeout does not reflect actual usage. Update activity timestamp on every authenticated request.
4. Client Clock Reliance for Expiry
Never trust the client's clock for expiry decisions. All expiry calculations must happen server-side using server timestamps.
5. Inconsistent Expiry Across Services
If multiple services handle refresh, they must agree on expiry. Use a centralized session store (Redis) to maintain consistent expiry state.
Practice Questions
- What is the difference between absolute and sliding window expiry?
- Why should idle timeout be tracked server-side rather than client-side?
- How does hybrid expiry combine both approaches?
- What happens when the absolute expiry is reached mid-refresh?
- Why should different client types have different expiry policies?
Answers:
- Absolute expiry sets a fixed session lifetime (e.g., 24 hours from login). Sliding window extends the session on each activity within a window (e.g., 1 hour idle timeout).
- The client can manipulate its clock or connection. Server-side tracking using Redis ensures consistent, tamper-proof idle detection.
- A sliding window extends the session on activity, up to a maximum absolute lifetime. The user never exceeds the absolute cap regardless of activity.
- The refresh request is rejected, forcing the user to re-authenticate. The client should gracefully handle this by redirecting to login.
- Web apps with frequent activity can use shorter windows. Mobile apps with intermittent usage need longer windows. Partner integrations should be short-lived for security.
Challenge: Build a session management system with configurable hybrid expiry, idle timeout detection via Redis, and a client that displays remaining session time and idle warnings.
FAQ
Mini Project
Build a session management system with hybrid expiry policies per client type, Redis-backed idle timeout detection, session status endpoints, and a frontend that shows remaining session time with re-authentication prompt.
What's Next
Now learn about Multi-Factor Authentication with TOTP for adding an extra layer of security to token-based auth.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro