Token Management
Learn how to properly manage JWT tokens, handle expiration, and implement token refresh.
Token Types
Access Token
- Lifetime: 15 minutes
- Purpose: Authenticates API requests
- Storage: In-memory (frontend) or secure cookie
- Revocation: Cannot be individually revoked (by design)
Refresh Token
- Lifetime: 7 days
- Purpose: Obtain new access tokens
- Storage: HTTP-only secure cookie or secure storage
- Revocation: Can be revoked immediately
Token Refresh
Endpoint
POST /auth/refresh
Request
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response (200 OK)
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Token Rotation
Both tokens are new after refresh. The old refresh token is immediately invalidated. This is a security feature that prevents token reuse attacks.
Error Responses
401 Unauthorized — Invalid Refresh Token
{
"type": "https://api.cortex.purplelab.ai/errors/invalid-token",
"title": "Invalid Token",
"status": 401,
"detail": "The refresh token is invalid or has been revoked",
"instance": "/auth/refresh"
}
Implementing Token Refresh
Automatic Refresh Pattern
class AuthClient {
private accessToken: string | null = null;
private refreshToken: string | null = null;
private refreshPromise: Promise<void> | null = null;
async fetch(url: string, options: RequestInit = {}): Promise<Response> {
// Add authorization header
const headers = new Headers(options.headers);
if (this.accessToken) {
headers.set('Authorization', `Bearer ${this.accessToken}`);
}
const response = await fetch(url, { ...options, headers });
// If 401, try to refresh
if (response.status === 401 && this.refreshToken) {
await this.refresh();
// Retry the request with new token
headers.set('Authorization', `Bearer ${this.accessToken}`);
return fetch(url, { ...options, headers });
}
return response;
}
private async refresh(): Promise<void> {
// Prevent multiple concurrent refresh requests
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = (async () => {
try {
const response = await fetch('/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken }),
});
if (!response.ok) {
throw new Error('Refresh failed');
}
const data = await response.json();
this.accessToken = data.accessToken;
this.refreshToken = data.refreshToken;
} finally {
this.refreshPromise = null;
}
})();
return this.refreshPromise;
}
}
Proactive Refresh
Instead of waiting for a 401, refresh proactively before expiration:
function isTokenExpiringSoon(token: string, thresholdSeconds = 60): boolean {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const expiresAt = payload.exp * 1000; // Convert to milliseconds
return Date.now() > expiresAt - thresholdSeconds * 1000;
} catch {
return true; // Treat parsing errors as expired
}
}
// Before each request
if (isTokenExpiringSoon(accessToken, 60)) {
await refreshTokens();
}
Logout
Endpoint
POST /auth/logout
Request
Include the access token in the Authorization header:
curl -X POST http://localhost:8091/auth/logout \
-H "Authorization: Bearer <access-token>"
Response (204 No Content)
No response body on success.
What Happens on Logout
- The refresh token is invalidated
- The session is terminated
- The access token remains valid until expiration (cannot be revoked)
Best Practice
On logout, also clear tokens from client storage to prevent accidental reuse.
Security Best Practices
Do
- Store refresh tokens securely (HTTP-only cookies or secure storage)
- Implement token refresh before expiration
- Clear tokens on logout
- Use HTTPS in production
Don't
- Store tokens in localStorage (XSS vulnerable)
- Include tokens in URLs
- Log tokens
- Share tokens between browser tabs (use a token manager)
Token Payload Inspection
To decode a JWT token (for debugging):
function decodeToken(token: string): any {
const payload = token.split('.')[1];
return JSON.parse(atob(payload));
}
const payload = decodeToken(accessToken);
console.log('User ID:', payload.sub);
console.log('Expires:', new Date(payload.exp * 1000));
Warning
Never trust client-side token validation for authorization. Always verify tokens on the server.