Skip to main content

Refresh Tokens

Refresh tokens allow applications to obtain new access tokens without requiring the user to re-authenticate. They have a longer lifetime than access tokens and are used to maintain user sessions.

When to Use

  • Maintaining long-lived user sessions
  • Obtaining new access tokens when the current one expires
  • Avoiding repeated user authentication

Token Lifetimes

Token TypeDefault LifetimePurpose
Access Token15 minutesShort-lived, used for API requests
Refresh Token30 daysLong-lived, used to get new access tokens

Flow Diagram

┌──────────┐                              ┌──────────────┐                              ┌──────────┐
│ User │ │ Your App │ │ S-Auth │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ (Initial auth - gets access + refresh tokens) │
│ │ │
│ 1. Make API request │ │
│ ─────────────────────────────────────────>│ │
│ │ │
│ │ 2. Token expired! │
│ │ │
│ │ 3. POST /token with refresh_token │
│ │ ─────────────────────────────────────────>│
│ │ │
│ │ 4. Return new access token │
│ │ <─────────────────────────────────────────│
│ │ │
│ │ 5. Retry original request │
│ 6. Success! │ │
│ <─────────────────────────────────────────│ │
│ │ │

Token Refresh Request

curl -X POST https://auth.sebbyk.net/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic BASE64(client_id:client_secret)" \
-d "grant_type=refresh_token" \
-d "refresh_token=srt_xyz789..."

Request Parameters

ParameterRequiredDescription
grant_typeYesMust be refresh_token
refresh_tokenYesThe refresh token to use
scopeNoOptionally request fewer scopes

Authentication

Confidential clients must authenticate:

Authorization: Basic BASE64(client_id:client_secret)

Public clients (PKCE) include client_id:

client_id=YOUR_CLIENT_ID

Token Response

{
"access_token": "sat_newtoken123...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "srt_newrefresh456...",
"scope": "openid profile email"
}

Note: S-Auth implements refresh token rotation - a new refresh token is issued with each refresh, and the old one is invalidated.

Example: Automatic Token Refresh

class TokenManager {
private accessToken: string;
private refreshToken: string;
private expiresAt: number;
private clientId: string;
private clientSecret: string;

constructor(initialTokens: {
access_token: string;
refresh_token: string;
expires_in: number;
}) {
this.accessToken = initialTokens.access_token;
this.refreshToken = initialTokens.refresh_token;
this.expiresAt = Date.now() + initialTokens.expires_in * 1000;
this.clientId = process.env.CLIENT_ID!;
this.clientSecret = process.env.CLIENT_SECRET!;
}

async getValidAccessToken(): Promise<string> {
// Check if token is expired or about to expire (60s buffer)
if (Date.now() >= this.expiresAt - 60000) {
await this.refresh();
}
return this.accessToken;
}

private async refresh(): Promise<void> {
const response = await fetch('https://auth.sebbyk.net/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${Buffer.from(
`${this.clientId}:${this.clientSecret}`
).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
}),
});

if (!response.ok) {
if (response.status === 400) {
// Refresh token is invalid/expired - user needs to re-login
throw new Error('SESSION_EXPIRED');
}
throw new Error(`Token refresh failed: ${response.status}`);
}

const data = await response.json();

this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
this.expiresAt = Date.now() + data.expires_in * 1000;
}

async makeRequest(url: string, options: RequestInit = {}): Promise<Response> {
const token = await this.getValidAccessToken();

const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
},
});

// If we get 401, try refreshing and retry once
if (response.status === 401) {
await this.refresh();
const newToken = this.accessToken;

return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newToken}`,
},
});
}

return response;
}
}

Example: React Hook for Token Management

import { useCallback, useRef } from 'react';

export function useTokenManager(initialTokens: {
accessToken: string;
refreshToken: string;
expiresIn: number;
}) {
const tokens = useRef({
accessToken: initialTokens.accessToken,
refreshToken: initialTokens.refreshToken,
expiresAt: Date.now() + initialTokens.expiresIn * 1000,
});

const refresh = useCallback(async () => {
const response = await fetch('https://auth.sebbyk.net/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: tokens.current.refreshToken,
client_id: import.meta.env.VITE_CLIENT_ID,
}),
});

if (!response.ok) {
// Redirect to login
window.location.href = '/login';
throw new Error('Session expired');
}

const data = await response.json();
tokens.current = {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + data.expires_in * 1000,
};

return data.access_token;
}, []);

const getAccessToken = useCallback(async () => {
if (Date.now() >= tokens.current.expiresAt - 60000) {
return refresh();
}
return tokens.current.accessToken;
}, [refresh]);

return { getAccessToken, refresh };
}

Error Responses

Invalid Refresh Token

{
"error": "invalid_grant",
"error_description": "Refresh token is invalid or expired"
}

Token Already Used (Rotation)

{
"error": "invalid_grant",
"error_description": "Refresh token has already been used"
}

Revoked Token

{
"error": "invalid_grant",
"error_description": "Token has been revoked"
}

Token Rotation

S-Auth implements refresh token rotation for security:

  1. Each time a refresh token is used, a new one is issued
  2. The old refresh token is invalidated
  3. If an old refresh token is used, all tokens for that grant are revoked (potential token theft)

This means:

  • Always store the new refresh token from each response
  • Never retry with an old refresh token

Revoking Tokens

To revoke a refresh token (logout):

curl -X POST https://auth.sebbyk.net/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic BASE64(client_id:client_secret)" \
-d "token=srt_xyz789..." \
-d "token_type_hint=refresh_token"

Security Best Practices

  1. Store securely - Use HttpOnly cookies or encrypted storage for refresh tokens
  2. Never expose to JavaScript in browsers if possible
  3. Implement token rotation - S-Auth does this automatically
  4. Handle rotation properly - Always save the new refresh token
  5. Revoke on logout - Call the revoke endpoint when users log out
  6. Monitor for reuse - Token reuse may indicate theft