Token Yenileme
Access token'ların refresh token ile yenilenmesi.
Neden Token Refresh?
Access token'lar kısa ömürlüdür (15 dakika). Kullanıcıyı her 15 dakikada tekrar login sayfasına göndermek kötü UX'tir.
Çözüm: Refresh token ile arka planda yeni access token al.
Refresh Token Alma
Authorization Request
offline_access scope'u ile refresh token alınır:
const authUrl = new URL('https://id.codeatlantis.com/oauth/authorize');
authUrl.searchParams.set('scope', 'openid profile email offline_access');
// ...
Token Response
{
"access_token": "eyJhbGci...",
"id_token": "eyJhbGci...",
"refresh_token": "RT_xxxxxxxxxxxxxxxxxxxxxxxx",
"expires_in": 900,
"token_type": "Bearer"
}
Refresh Token Flow
┌─────────┐ ┌──────────────┐
│ Client │ │ Atlantic ID │
└────┬────┘ └──────┬───────┘
│ │
│ POST /oauth/token │
│ grant_type=refresh_token │
│ refresh_token=RT_xxx │
│ ───────────────────────────────▶
│ │
│ Response: │
│ - new access_token │
│ - new id_token │
│ - new refresh_token (rotation) │
│◀───────────────────────────────│
Implementation
Token Refresh Request
async function refreshAccessToken(refreshToken) {
const response = await fetch('https://id.codeatlantis.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa(clientId + ':' + clientSecret)}`
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
// Opsiyonel: Scope daraltma
// scope: 'openid profile'
})
});
if (!response.ok) {
throw new Error('Refresh failed');
}
return await response.json();
}
Response
{
"access_token": "eyJhbGci...", // YENİ
"id_token": "eyJhbGci...", // YENİ
"refresh_token": "RT_yyyyyy...", // YENİ (rotation)
"expires_in": 900,
"token_type": "Bearer"
}
Refresh Token Rotation
Güvenlik için refresh token her kullanımda yenilenir:
async function refreshAndStore(oldRefreshToken) {
const tokens = await refreshAccessToken(oldRefreshToken);
// ÖNEMLİ: Yeni refresh token'ı kaydet!
req.session.refreshToken = tokens.refresh_token;
req.session.accessToken = tokens.access_token;
req.session.expiresAt = Date.now() + (tokens.expires_in * 1000);
await req.session.save();
return tokens;
}
Uyarı: Eski refresh token artık geçersizdir!
Automatic Refresh
Middleware Approach
async function ensureValidToken(req, res, next) {
if (!req.session.userId) {
return res.redirect('/login');
}
// Token hala geçerli mi?
const timeLeft = req.session.expiresAt - Date.now();
if (timeLeft < 5 * 60 * 1000) { // 5 dakikadan az kaldı
try {
await refreshAndStore(req.session.refreshToken);
} catch (error) {
// Refresh başarısız → logout
req.session.destroy();
return res.redirect('/login');
}
}
next();
}
// Protected route'larda kullan
app.get('/dashboard', ensureValidToken, (req, res) => {
res.render('dashboard');
});
Proactive Refresh
Token expire olmadan önce yenile:
// Her 10 dakikada bir kontrol et
setInterval(async () => {
const expiresAt = req.session.expiresAt;
const timeLeft = expiresAt - Date.now();
// 5 dakikadan az kaldıysa yenile
if (timeLeft < 5 * 60 * 1000) {
await refreshAndStore(req.session.refreshToken);
}
}, 10 * 60 * 1000);
Frontend Implementation
Axios Interceptor
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// 401 + token expired
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Backend'de refresh yap
await fetch('/api/auth/refresh', { method: 'POST' });
// Orijinal isteği tekrarla
return axios(originalRequest);
} catch (refreshError) {
// Refresh başarısız → login
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
Fetch Wrapper
async function authenticatedFetch(url, options = {}) {
let response = await fetch(url, {
...options,
credentials: 'include'
});
// Token expired → refresh and retry
if (response.status === 401) {
const refreshed = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
if (!refreshed.ok) {
window.location.href = '/login';
throw new Error('Session expired');
}
// Retry original request
response = await fetch(url, {
...options,
credentials: 'include'
});
}
return response;
}
Error Handling
Refresh Failed
async function handleRefreshError(error) {
if (error.error === 'invalid_grant') {
// Refresh token invalid/expired
// User'ı logout et
await logout();
window.location.href = '/login?reason=session_expired';
} else {
// Geçici hata → retry
console.error('Refresh error:', error);
}
}
Token Revoked
{
"error": "invalid_grant",
"error_description": "The refresh token has been revoked"
}
Handling:
if (error.error === 'invalid_grant') {
// Token revoked/expired
// Force re-authentication
req.session.destroy();
res.redirect('/login?reason=token_revoked');
}
Security Best Practices
✅ Do
- Store refresh token securely - Backend only, encrypted
- Rotate on use - Her refresh'te yeni token
- Set expiration - 30 gün (configurable)
- Revoke on logout - Logout'ta refresh token'ı iptal et
- Use HTTPS - Token'lar asla HTTP'de gönderilmemeli
❌ Don't
- Store in localStorage - XSS riski
- Send to frontend - Refresh token frontend'e gitmemeli
- Reuse old tokens - Rotation sonrası eski token geçersiz
- Ignore errors - Refresh fail → logout
- Share across clients - Her client kendi refresh token'ına sahip
Refresh Token Lifecycle
1. Authorization → Refresh token (RT1) alındı
↓
2. Access token expired → RT1 ile refresh
↓
3. New tokens: access_token2, RT2 (RT1 geçersiz)
↓
4. Access token2 expired → RT2 ile refresh
↓
5. New tokens: access_token3, RT3 (RT2 geçersiz)
↓
... (30 gün veya logout'a kadar)
↓
30. gün veya Logout → All tokens revoked
Testing
describe('Token Refresh', () => {
it('should refresh expired access token', async () => {
// Mock expired access token
const expiredToken = createExpiredToken();
// Try to use it
const response = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${expiredToken}`);
// Should auto-refresh and succeed
expect(response.status).toBe(200);
});
it('should handle refresh failure', async () => {
// Mock invalid refresh token
const invalidRefreshToken = 'invalid';
const response = await refreshAccessToken(invalidRefreshToken);
expect(response.error).toBe('invalid_grant');
});
});
Monitoring
// Track refresh events
const refreshMetrics = {
count: 0,
failures: 0,
recordRefresh(success) {
this.count++;
if (!success) this.failures++;
// Alert if failure rate > 10%
if (this.failures / this.count > 0.1) {
console.warn('High refresh failure rate!');
}
}
};
İlgili: Session Management, JWT Tokens, Security