Token Refresh
When access tokens expire, use the refresh token to obtain new tokens without re-authenticating.
Endpoint
POST /api/v1/auth/refreshAuthentication: None required (refresh token in body)
Request
Headers
Content-Type: application/jsonBody
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}| Field | Type | Required | Description |
|---|---|---|---|
refreshToken | string | Yes | Valid refresh token from login |
Response
Success (200)
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}⚠️
Important: A new refresh token is returned. The old refresh token is invalidated and cannot be reused. Store the new refresh token for future refreshes.
Error Responses
Missing Token (400)
{
"error": "Refresh token is required"
}Invalid/Expired Token (401)
{
"error": "Invalid or expired refresh token"
}Wrong Token Type (401)
{
"error": "Invalid token type"
}Token Already Used (401)
{
"error": "Refresh token has already been used or revoked"
}User Not Found (401)
{
"error": "User not found"
}Example
cURL
curl -X POST https://your-instance.com/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}'JavaScript
async function refreshTokens(refreshToken) {
const response = await fetch('/api/v1/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
// Refresh failed - redirect to login
throw new Error('Session expired');
}
const { accessToken, refreshToken: newRefreshToken } = await response.json();
// Store BOTH new tokens
storeAccessToken(accessToken);
storeRefreshToken(newRefreshToken); // Important: use the NEW token
return accessToken;
}Token Rotation
Nexgent implements refresh token rotation for security:
1. Client sends refresh token
└─► Token is consumed (deleted from Redis)
2. Server validates token
└─► Checks signature, expiry, and Redis record
3. Server issues NEW token pair
└─► New access token (15 min)
└─► New refresh token (24h)
4. Old refresh token is invalidated
└─► Cannot be reused
└─► Reuse attempts are logged as potential theftWhy Rotation?
| Without Rotation | With Rotation |
|---|---|
| Stolen token works until expiry | Stolen token works only once |
| Attacker has 24h window | Legitimate use invalidates stolen token |
| No theft detection | Reuse attempts detected |
Handling Refresh in Your App
Proactive Refresh
Refresh before the access token expires:
// Refresh at 14 minutes (1 minute before 15-minute expiry)
const REFRESH_THRESHOLD_MS = 14 * 60 * 1000;
function scheduleRefresh(accessToken) {
const payload = parseJwt(accessToken);
const expiresAt = payload.exp * 1000;
const refreshAt = expiresAt - REFRESH_THRESHOLD_MS;
const delay = refreshAt - Date.now();
if (delay > 0) {
setTimeout(() => refreshTokens(), delay);
}
}Reactive Refresh (on 401)
Retry failed requests after refreshing:
async function apiRequest(url, options) {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${getAccessToken()}`,
},
});
if (response.status === 401) {
// Try to refresh
try {
await refreshTokens(getRefreshToken());
// Retry original request with new token
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${getAccessToken()}`,
},
});
} catch {
// Refresh failed - session expired
redirectToLogin();
throw new Error('Session expired');
}
}
return response;
}Common Issues
"Refresh token has already been used"
Cause: The same refresh token was used twice.
Solutions:
- Ensure you store the new refresh token from each refresh response
- Don't make concurrent refresh requests
- Use a mutex/lock for refresh operations
"Invalid or expired refresh token"
Cause: Token is malformed, expired, or was revoked.
Solutions:
- Check if user logged out (token was revoked)
- Check if token expired (24h default, 30 days with "Remember Me")
- Redirect to login for re-authentication
Race Conditions
Multiple tabs/requests refreshing simultaneously:
let refreshPromise = null;
async function refreshTokens() {
// Reuse existing refresh if in progress
if (refreshPromise) {
return refreshPromise;
}
refreshPromise = doRefresh();
try {
return await refreshPromise;
} finally {
refreshPromise = null;
}
}