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

  1. Store refresh token securely - Backend only, encrypted
  2. Rotate on use - Her refresh'te yeni token
  3. Set expiration - 30 gün (configurable)
  4. Revoke on logout - Logout'ta refresh token'ı iptal et
  5. Use HTTPS - Token'lar asla HTTP'de gönderilmemeli

❌ Don't

  1. Store in localStorage - XSS riski
  2. Send to frontend - Refresh token frontend'e gitmemeli
  3. Reuse old tokens - Rotation sonrası eski token geçersiz
  4. Ignore errors - Refresh fail → logout
  5. 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