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..."
}Only a new access token is returned. The same refresh token remains valid until it expires or is revoked on logout. You do not need to store a new refresh token.
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 Revoked (401)
{
"error": "Refresh token has been revoked"
}Returned when the refresh token was revoked (e.g., user logged out). Re-authenticate to obtain a new token pair.
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 } = await response.json();
// Store the new access token; refresh token stays the same
storeAccessToken(accessToken);
return accessToken;
}Refresh Token Behavior
Nexgent uses reusable refresh tokens:
1. Client sends refresh token
└─► Token is validated (signature, expiry, Redis record)
2. Server does NOT consume the refresh token
└─► Same token remains valid until expiry or logout
3. Server issues new access token only
└─► New access token (e.g. 15 min)
└─► Refresh token unchanged (e.g. 24h or 30 days with "Remember Me")
4. Refresh token is revoked only on explicit logout
└─► When client sends refresh token in logout body, it is revoked in RedisThis keeps the client simple (no need to store a new refresh token on every refresh) while still allowing immediate revocation on logout.
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 been revoked"
Cause: The refresh token was revoked (e.g., user logged out and the client sent the refresh token in the logout request).
Solutions:
- Redirect to login for re-authentication
- Obtain a new token pair via login or registration
"Invalid or expired refresh token"
Cause: Token is malformed or expired.
Solutions:
- Check if token expired (e.g. 24h default, 30 days with "Remember Me")
- If revoked, you will see "Refresh token has been revoked" instead
- 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;
}
}