Skip to main content

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

  1. The refresh token is invalidated
  2. The session is terminated
  3. 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.