Refresh Tokens
This document describes the refresh token functionality in the auth service.
Overview
Section titled “Overview”The auth service supports refresh tokens to provide a better user experience by allowing token renewal without re-authentication. When exchanging a Keycloak or Entra token, the service returns both an access token and a refresh token.
Token Flow
Section titled “Token Flow”sequenceDiagram participant Client participant AuthService participant IDP participant Database
Client->>IDP: Login IDP-->>Client: IDP Token
Client->>AuthService: POST /api/v1/auth/token<br/>(token exchange) AuthService->>AuthService: Validate IDP token AuthService->>Database: Create refresh token AuthService-->>Client: Access Token + Refresh Token
Note over Client: Access token expires...
Client->>AuthService: POST /api/v1/auth/token<br/>(refresh grant) AuthService->>Database: Validate refresh token AuthService->>Database: Rotate refresh token AuthService-->>Client: New Access Token + New Refresh TokenAPI Endpoints
Section titled “API Endpoints”Token Exchange (with Refresh Token)
Section titled “Token Exchange (with Refresh Token)”POST /api/v1/auth/tokenContent-Type: application/json
{ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "eyJhbGc...", "subject_token_type": "urn:ietf:params:oauth:token-type:access_token"}Response:
{ "access_token": "eyJhbGc...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "rt_2KqM3bPt9HcLwYv6xRjN8aZe4FgS7uDm", "refresh_expires_in": 2592000, "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "scope": "org:1:role:organization_owner"}Refresh Token Grant
Section titled “Refresh Token Grant”POST /api/v1/auth/tokenContent-Type: application/json
{ "grant_type": "refresh_token", "refresh_token": "rt_2KqM3bPt9HcLwYv6xRjN8aZe4FgS7uDm"}Response:
{ "access_token": "eyJhbGc...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "rt_7uDm4FgS8aZe3bPt9HcLwYv6xRjN2KqM", "refresh_expires_in": 2592000, "scope": "org:1:role:organization_owner"}Configuration
Section titled “Configuration”Refresh token behavior can be configured via environment variables:
| Variable | Default | Description |
|---|---|---|
REFRESH_TOKEN_EXPIRY_DAYS | 30 | How long refresh tokens are valid |
REFRESH_TOKEN_ROTATION | true | Whether to rotate tokens on use |
REFRESH_TOKEN_REUSE_WINDOW | 30 | Grace period (seconds) for concurrent requests |
REFRESH_TOKEN_MAX_CHAIN_LENGTH | 100 | Maximum number of refreshes from original token |
REFRESH_TOKEN_BIND_TO_IP | false | Validate IP address on refresh |
REFRESH_TOKEN_BIND_TO_DEVICE | false | Validate device fingerprint on refresh |
Security Features
Section titled “Security Features”1. Token Rotation
Section titled “1. Token Rotation”By default, refresh tokens are rotated on each use:
- Old token is revoked after a grace period
- New refresh token is issued with each refresh
- Prevents token replay attacks
2. Reuse Window
Section titled “2. Reuse Window”A configurable grace period allows for:
- Concurrent requests with the same refresh token
- Network retry scenarios
- Default: 30 seconds
3. Chain Length Limit
Section titled “3. Chain Length Limit”Prevents infinite token refresh chains:
- Each refresh token tracks its parent
- Maximum chain length enforced
- Default: 100 refreshes
4. Optional Binding
Section titled “4. Optional Binding”Refresh tokens can optionally be bound to:
- IP Address: Token only valid from same IP
- Device ID: Token only valid from same device
Database Schema
Section titled “Database Schema”CREATE TABLE refresh_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), token_hash VARCHAR(64) NOT NULL UNIQUE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, idp_provider VARCHAR(50) NOT NULL, idp_user_id VARCHAR(255) NOT NULL, issued_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), expires_at TIMESTAMP WITH TIME ZONE NOT NULL, last_used_at TIMESTAMP WITH TIME ZONE, ip_address INET, user_agent TEXT, device_id VARCHAR(255), parent_token_id UUID REFERENCES refresh_tokens(id) ON DELETE SET NULL, revoked_at TIMESTAMP WITH TIME ZONE, revocation_reason VARCHAR(50), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW());Frontend Integration
Section titled “Frontend Integration”JavaScript/TypeScript Example
Section titled “JavaScript/TypeScript Example”class AuthService { private accessToken: string; private refreshToken: string; private tokenExpiry: number;
async login(username: string, password: string) { // 1. Login with Keycloak const keycloakToken = await this.keycloakLogin(username, password);
// 2. Exchange for enriched tokens const response = await fetch("/api/v1/auth/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", subject_token: keycloakToken, subject_token_type: "urn:ietf:params:oauth:token-type:access_token", }), });
const data = await response.json(); this.accessToken = data.access_token; this.refreshToken = data.refresh_token; this.tokenExpiry = Date.now() + data.expires_in * 1000; }
async getAccessToken(): Promise<string> { // Check if token needs refresh (with 1 minute buffer) if (Date.now() > this.tokenExpiry - 60000) { await this.refreshAccessToken(); } return this.accessToken; }
private async refreshAccessToken() { const response = await fetch("/api/v1/auth/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ grant_type: "refresh_token", refresh_token: this.refreshToken, }), });
if (!response.ok) { // Refresh failed, need to re-authenticate throw new Error("Token refresh failed"); }
const data = await response.json(); this.accessToken = data.access_token; this.refreshToken = data.refresh_token; this.tokenExpiry = Date.now() + data.expires_in * 1000; }}Axios Interceptor Example
Section titled “Axios Interceptor Example”// Request interceptoraxios.interceptors.request.use(async (config) => { const token = await authService.getAccessToken(); config.headers.Authorization = `Bearer ${token}`; return config;});
// Response interceptor for automatic retryaxios.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true;
try { await authService.refreshAccessToken(); const token = await authService.getAccessToken(); originalRequest.headers.Authorization = `Bearer ${token}`; return axios(originalRequest); } catch (refreshError) { // Redirect to login window.location.href = "/login"; } }
return Promise.reject(error); },);Testing
Section titled “Testing”To verify refresh token functionality locally, call POST /api/v1/auth/token with a token-exchange request, then call the same endpoint with grant_type: "refresh_token" and the returned refresh_token. Confirm rotation behavior by retrying the old refresh token after the configured reuse window.
Monitoring
Section titled “Monitoring”Monitor refresh token usage through logs:
# Token issuancegrep "Generated new refresh token" auth-service.log
# Token refreshgrep "Rotated refresh token" auth-service.log
# Failed refresh attemptsgrep "Invalid refresh token" auth-service.logTroubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”1. Refresh token not returned
- Check if Keycloak or Entra is configured; token exchange is not registered for Auth0 or generic OIDC
- Verify refresh token service initialization
- Check database migrations
2. Refresh fails with “invalid_grant”
- Token may be expired (check
REFRESH_TOKEN_EXPIRY_DAYS) - Token may have been rotated (check logs)
- IP/Device binding may be failing
3. Old refresh token still works
- Check if rotation is enabled (
REFRESH_TOKEN_ROTATION) - May be within reuse window (
REFRESH_TOKEN_REUSE_WINDOW)
4. Maximum chain length exceeded
- User has refreshed token too many times
- Increase
REFRESH_TOKEN_MAX_CHAIN_LENGTHor re-authenticate
Best Practices
Section titled “Best Practices”-
Store tokens securely
- Use secure storage (not localStorage for sensitive apps)
- Consider using httpOnly cookies for web apps
-
Handle refresh failures gracefully
- Implement retry logic with exponential backoff
- Redirect to login on permanent failure
-
Monitor token usage
- Track refresh patterns
- Alert on suspicious activity
-
Set appropriate expiry times
- Short access tokens (1 hour)
- Longer refresh tokens (30 days)
- Balance security with user experience