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 Type | Default Lifetime | Purpose |
|---|---|---|
| Access Token | 15 minutes | Short-lived, used for API requests |
| Refresh Token | 30 days | Long-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
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be refresh_token |
refresh_token | Yes | The refresh token to use |
scope | No | Optionally 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:
- Each time a refresh token is used, a new one is issued
- The old refresh token is invalidated
- 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
- Store securely - Use HttpOnly cookies or encrypted storage for refresh tokens
- Never expose to JavaScript in browsers if possible
- Implement token rotation - S-Auth does this automatically
- Handle rotation properly - Always save the new refresh token
- Revoke on logout - Call the revoke endpoint when users log out
- Monitor for reuse - Token reuse may indicate theft