مقدمه
اگر در پروژههای PHP، API یا سیستمهای مبتنی بر OAuth 2.0 کار کرده باشید، احتمالاً با دو مفهوم زیر برخورد کردهاید:
Access Token
Refresh Token
بسیاری از توسعهدهندگان در ابتدا این دو Token را شبیه هم در نظر میگیرند، در حالی که کاربرد و سطح ریسک آنها متفاوت است.
Access Token معمولاً برای دسترسی به API استفاده میشود و عمر نسبتاً کوتاهی دارد.
Refresh Token برای دریافت Access Token جدید استفاده میشود و معمولاً عمر بیشتری دارد.
به همین دلیل، اگر Refresh Token سرقت شود، مهاجم ممکن است بتواند در مدت طولانیتری به سیستم دسترسی پیدا کند.
در این مقاله میخواهیم از صفر بررسی کنیم:
- Refresh Token چیست؟
- چه تفاوتی با Access Token دارد؟
- چرا Access Token باید عمر کوتاهی داشته باشد؟
- Refresh Token را کجا ذخیره کنیم؟
- Token Rotation چیست؟
- Token Revocation چیست؟
- اگر Refresh Token سرقت شود چه اتفاقی میافتد؟
- چگونه Refresh Token را در PHP به شکل امن پیادهسازی کنیم؟
- چگونه Logout را با Revocation ترکیب کنیم؟
- چگونه Token Family را مدیریت کنیم؟
- چه زمانی JWT و چه زمانی Token تصادفی مناسبتر است؟
# 1. Access Token چیست؟
Access Token مجوزی است که Client با استفاده از آن به یک Resource Server یا API درخواست میفرستد.
مثلاً:
GET /api/profile
Authorization: Bearer ACCESS_TOKEN
سرور Token را بررسی میکند و اگر معتبر باشد، درخواست را پردازش میکند.
ساختار ساده:
User
↓
Login
↓
Access Token
↓
API Request
↓
Resource Server
# 2. چرا Access Token نباید عمر بسیار طولانی داشته باشد؟
فرض کنید Access Token شما یک سال اعتبار داشته باشد.
اگر این Token به سرقت برود، مهاجم ممکن است تا پایان عمر Token بتواند از آن استفاده کند.
به همین دلیل معمولاً Access Token عمر نسبتاً کوتاهی دارد.
مثلاً:
Access Token
Expiration: 15 minutes
یا:
Access Token
Expiration: 1 hour
مقدار مناسب به معماری و سطح ریسک سیستم بستگی دارد.
اما یک سؤال ایجاد میشود:
اگر Access Token منقضی شد، آیا کاربر باید دوباره Login کند؟
لزومی ندارد.
اینجاست که Refresh Token وارد میشود.
# 3. Refresh Token چیست؟
Refresh Token یک Credential است که Client میتواند با استفاده از آن Access Token جدید دریافت کند.
جریان کلی:
Login
↓
Access Token
+
Refresh Token
↓
Access Token expires
↓
Refresh Token
↓
New Access Token
بنابراین کاربر مجبور نیست برای هر Access Token جدید دوباره Username و Password خود را وارد کند.
# 4. تفاوت Access Token و Refresh Token
| ویژگی | Access Token | Refresh Token |
|---|---|---|
| کاربرد | دسترسی به API | دریافت Access Token جدید |
| طول عمر | معمولاً کوتاهتر | معمولاً طولانیتر |
| ارسال به API | بله | معمولاً خیر |
| سطح حساسیت | بالا | بسیار بالا |
| نیاز به Revocation | مهم | بسیار مهم |
| مناسب برای Browser API Request | بسته به معماری | معمولاً خیر |
یک نکته مهم:
Refresh Token نباید در هر درخواست API ارسال شود.
# 5. یک سناریوی واقعی
فرض کنید کاربر وارد حساب خود شده است.
سرور دو Token صادر میکند:
{
"access_token": "access-token-123",
"refresh_token": "refresh-token-456",
"expires_in": 900
}
Access Token:
15 دقیقه
اعتبار دارد.
بعد از 15 دقیقه:
Access Token
↓
Expired
Client به جای Login مجدد، Refresh Token را ارسال میکند:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=refresh-token-456
سرور Refresh Token را بررسی میکند.
اگر معتبر باشد، Token جدید صادر میکند.
# 6. چرا Refresh Token حساستر است؟
فرض کنید Access Token شما فقط 15 دقیقه اعتبار دارد.
اگر دزدیده شود، پنجره سوءاستفاده محدودتر است.
اما Refresh Token ممکن است هفتهها یا ماهها اعتبار داشته باشد.
بنابراین:
Refresh Token
↓
Long-lived Credential
↓
High Security Risk
به همین دلیل نگهداری و مدیریت آن بسیار مهم است.
# 7. Refresh Token را نباید مثل Password ذخیره کرد
یکی از رویکردهای مناسب این است که Refresh Token تصادفی تولید کنیم و نسخه Hash شده آن را در دیتابیس ذخیره کنیم.
مثلاً Token اصلی:
RANDOM_REFRESH_TOKEN
و در دیتابیس:
SHA256(RANDOM_REFRESH_TOKEN)
ذخیره شود.
مشابه رویکردی که برای Reset Token یا Email Verification Token استفاده میکنیم.
# 8. تولید Refresh Token امن
از random_bytes() استفاده کنید.
مثلاً:
$refreshToken = bin2hex(
random_bytes(32)
);
این Token دارای 64 کاراکتر hexadecimal خواهد بود.
سپس Hash آن:
$refreshTokenHash = hash(
'sha256',
$refreshToken
);
را ذخیره میکنیم.
# 9. چرا از rand() استفاده نکنیم؟
این کار مناسب نیست:
$token = rand();
یا:
$token = mt_rand();
برای Credentialهای امنیتی باید از منبع تصادفی رمزنگاریشده استفاده کنیم:
random_bytes()
# 10. ساخت جدول Refresh Token
یک جدول ساده میتواند چنین ساختاری داشته باشد:
CREATE TABLE refresh_tokens (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
token_hash CHAR(64) NOT NULL UNIQUE,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_expires_at (expires_at),
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE
);
این جدول اطلاعاتی مانند:
User
Token Hash
Expiration
Revocation
Creation Time
را نگه میدارد.
# 11. چرا Raw Token را در Database ذخیره نکنیم؟
فرض کنید دیتابیس شما لو برود.
اگر Refresh Token خام ذخیره شده باشد:
Database Leak
↓
Raw Refresh Tokens
↓
Potential Account Access
اما اگر فقط Hash ذخیره شده باشد:
Database Leak
↓
Token Hashes
مهاجم نمیتواند صرفاً با دیدن مقدار Hash، همان Token اصلی را مستقیماً به سرور ارائه کند.
البته Hash کردن جایگزین تمام اقدامات امنیتی دیگر نیست.
# 12. فرآیند صدور Refresh Token
بعد از Login:
$refreshToken = bin2hex(
random_bytes(32)
);
$tokenHash = hash(
'sha256',
$refreshToken
);
سپس:
$stmt = $pdo->prepare(
'INSERT INTO refresh_tokens
(user_id, token_hash, expires_at)
VALUES (?, ?, ?)'
);
$stmt->execute([
$userId,
$tokenHash,
$expiresAt
]);
و Raw Token را فقط در پاسخ به Client قرار میدهیم:
echo json_encode([
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
]);
# 13. Refresh Endpoint
فرض کنیم Endpoint زیر را داریم:
POST /oauth/token
و Client درخواست زیر را میفرستد:
grant_type=refresh_token
refresh_token=...
در PHP:
$refreshToken = $_POST['refresh_token'] ?? null;
if (!$refreshToken) {
http_response_code(400);
exit('Refresh token is required.');
}
سپس Hash را محاسبه میکنیم:
$tokenHash = hash(
'sha256',
$refreshToken
);
# 14. پیدا کردن Refresh Token
حالا دیتابیس را جستجو میکنیم:
SELECT *
FROM refresh_tokens
WHERE token_hash = ?
LIMIT 1;
در PHP:
$stmt = $pdo->prepare(
'SELECT *
FROM refresh_tokens
WHERE token_hash = ?
LIMIT 1'
);
$stmt->execute([
$tokenHash
]);
$tokenRecord = $stmt->fetch();
اگر Token وجود نداشته باشد:
if (!$tokenRecord) {
http_response_code(401);
exit('Invalid refresh token.');
}
# 15. بررسی Expiration
نباید فقط وجود Token را بررسی کنیم.
باید ببینیم منقضی نشده باشد.
مثلاً:
if (
strtotime($tokenRecord['expires_at'])
<= time()
) {
http_response_code(401);
exit('Refresh token expired.');
}
# 16. بررسی Revocation
همچنین باید بررسی کنیم که Token قبلاً Revoked نشده باشد.
if ($tokenRecord['revoked_at'] !== null) {
http_response_code(401);
exit('Refresh token revoked.');
}
پس Token معتبر باید:
Exists
+
Not Expired
+
Not Revoked
باشد.
# 17. Token Rotation چیست؟
یکی از مهمترین مفاهیم امنیتی Refresh Token:
Refresh Token Rotation
است.
در روش ساده، Client بارها از همان Refresh Token استفاده میکند:
Refresh Token A
↓
Access Token B
Refresh Token A
↓
Access Token C
Refresh Token A
↓
Access Token D
این یعنی یک Refresh Token برای مدت طولانی قابل استفاده است.
در Rotation، هر بار که Refresh Token مصرف میشود:
Old Refresh Token
↓
Revoked
↓
New Refresh Token
تولید میشود.
# 18. Token Rotation چگونه کار میکند؟
فرض کنیم ابتدا داریم:
Refresh Token A
Client آن را استفاده میکند.
سرور:
A → Revoke
A → Generate B
پاسخ:
{
"access_token": "new-access-token",
"refresh_token": "refresh-token-B"
}
از این لحظه:
A = Invalid
B = Valid
اگر Client دوباره A را ارسال کند، باید رد شود.
# 19. مزیت Token Rotation
فرض کنید مهاجم Refresh Token A را سرقت کرده باشد.
کاربر واقعی قبل از مهاجم از A استفاده میکند:
A
↓
Valid
↓
Rotate
↓
B
حالا A دیگر معتبر نیست.
اگر مهاجم بعداً A را استفاده کند:
A
↓
Revoked
↓
Rejected
این موضوع میتواند پنجره سوءاستفاده از یک Refresh Token سرقتشده را کاهش دهد.
# 20. Refresh Token Family
برای سیستمهای حرفهایتر بهتر است Refresh Tokenها را در قالب یک Token Family مدیریت کنیم.
مثلاً:
Family: F1
Token A
↓
Token B
↓
Token C
↓
Token D
هر Refresh Token به یک Family تعلق دارد.
مثلاً جدول:
ALTER TABLE refresh_tokens
ADD COLUMN family_id CHAR(64) NOT NULL;
# 21. چرا Token Family مهم است؟
فرض کنید:
A
↓
B
↓
C
و Token B دزدیده شده باشد.
کاربر واقعی از B استفاده کرده و C دریافت کرده است.
مهاجم بعداً B را ارسال میکند.
این یک علامت مهم است:
Previously Used Token
↓
Reuse Detection
میتوانیم این وضعیت را بهعنوان احتمال Token Theft در نظر بگیریم و Family مربوطه را باطل کنیم.
# 22. Token Reuse Detection
یکی از قابلیتهای مهم سیستمهای حرفهای:
Refresh Token Reuse Detection
است.
مثلاً:
A → B → C
اگر A قبلاً Rotate شده و دوباره استفاده شود:
A
↓
Already Revoked
↓
Reuse Detected
سرور میتواند تمام Tokenهای Family را Revoked کند.
Family F1
├── A → Revoked
├── B → Revoked
└── C → Revoked
در نتیجه Sessionهای وابسته به آن Family نیاز به احراز هویت مجدد خواهند داشت.
# 23. طراحی کاملتر جدول Token
برای چنین سیستمی میتوانیم جدول را توسعه دهیم:
CREATE TABLE refresh_tokens (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
family_id CHAR(64) NOT NULL,
token_hash CHAR(64) NOT NULL UNIQUE,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_family_id (family_id),
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE
);
# 24. ایجاد Family
هنگام Login:
$familyId = bin2hex(
random_bytes(32)
);
سپس Token را ایجاد میکنیم:
$refreshToken = bin2hex(
random_bytes(32)
);
Hash:
$tokenHash = hash(
'sha256',
$refreshToken
);
و در دیتابیس ذخیره میکنیم:
$stmt = $pdo->prepare(
'INSERT INTO refresh_tokens
(
user_id,
family_id,
token_hash,
expires_at
)
VALUES (?, ?, ?, ?)'
);
$stmt->execute([
$userId,
$familyId,
$tokenHash,
$expiresAt
]);
# 25. Rotation به شکل Transaction
Rotation باید با دقت انجام شود.
بهتر است عملیات تغییر Token داخل Transaction انجام شود:
$pdo->beginTransaction();
try {
// Revoke old token
// Create new refresh token
// Commit
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
چرا؟
چون نمیخواهیم وضعیت ناقصی ایجاد شود.
مثلاً:
Old Token Revoked
New Token Not Created
یا:
New Token Created
Old Token Still Valid
# 26. نمونه Rotation
ابتدا Token قبلی را با قفل مناسب پیدا میکنیم.
در MySQL میتوانیم در یک Transaction از:
SELECT *
FROM refresh_tokens
WHERE token_hash = ?
FOR UPDATE;
استفاده کنیم.
بعد:
if ($tokenRecord['revoked_at'] !== null) {
// Reuse detected
$pdo->rollBack();
http_response_code(401);
exit('Invalid refresh token.');
}
سپس Token قدیمی را Revoked میکنیم:
UPDATE refresh_tokens
SET revoked_at = NOW()
WHERE id = ?
و Token جدید ایجاد میکنیم.
# 27. تولید Access Token جدید
بعد از اعتبارسنجی Refresh Token، باید Access Token جدید صادر کنیم.
برای مثال اگر از JWT استفاده میکنیم:
sub
iat
exp
iss
aud
scope
میتوانند Claimهای آن باشند.
مثلاً:
{
"sub": "123",
"iat": 1760000000,
"exp": 1760000900,
"iss": "https://example.com",
"aud": "api"
}
توجه کنید که ساخت و امضای JWT باید با الگوریتم مناسب و کتابخانه معتبر انجام شود.
# 28. Refresh Token لازم نیست JWT باشد
یک اشتباه رایج این است که تصور کنیم Refresh Token حتماً باید JWT باشد.
لزومی ندارد.
میتوانیم Refresh Token را یک مقدار کاملاً تصادفی قرار دهیم:
random opaque token
مثلاً:
8e1c...
و فقط Hash آن را در دیتابیس ذخیره کنیم.
برای بسیاری از سیستمها این روش ساده و قابل کنترل است.
# 29. Opaque Token چیست؟
Opaque Token یعنی Tokenای که اطلاعات قابل استفادهای برای Client یا Resource Server داخل خودش ندارد.
مثلاً:
8a91c4f7...
سرور برای فهمیدن وضعیت Token باید آن را در دیتابیس یا Storage جستجو کند.
در مقابل JWT معمولاً اطلاعاتی مانند:
sub
iss
aud
exp
را داخل Payload دارد.
# 30. JWT Refresh Token یا Opaque Refresh Token؟
این انتخاب به معماری سیستم بستگی دارد.
JWT
مزایا:
- قابل اعتبارسنجی بدون Lookup دائمی
- مناسب برخی معماریهای توزیعشده
- امکان حمل Claims
معایب:
- Revocation پیچیدهتر
- مدیریت Logout پیچیدهتر
- در صورت سرقت، کنترل آن دشوارتر
Opaque Token
مزایا:
- کنترل کامل Server
- Revocation ساده
- Rotation ساده
- ذخیره Hash در Database
معایب:
- نیاز به Storage Lookup
- نیاز به طراحی مناسب Database/Cache
برای Refresh Token، Opaque Random Token در بسیاری از طراحیها انتخاب سادهای است.
# 31. Refresh Token در Browser
محل ذخیره Refresh Token در Browser موضوع بسیار مهمی است.
قرار دادن Credential حساس در:
localStorage
میتواند در صورت وجود XSS، ریسک سرقت Token را افزایش دهد.
در معماریهای Browser-based معمولاً باید Storage و Cookie را با توجه به مدل Threat سیستم انتخاب کرد.
اگر Refresh Token را در Cookie قرار میدهید، ویژگیهایی مانند:
HttpOnly
Secure
SameSite
اهمیت پیدا میکنند.
همچنین باید موضوع CSRF را نیز در طراحی در نظر بگیرید.
# 32. HttpOnly چیست؟
اگر Cookie دارای:
HttpOnly
باشد، JavaScript معمولی Browser نمیتواند آن را از طریق document.cookie بخواند.
مثلاً:
setcookie(
'refresh_token',
$refreshToken,
[
'expires' => time() + 2592000,
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]
);
اما HttpOnly بهتنهایی همه حملات Web را حل نمیکند.
# 33. Secure Cookie
ویژگی:
Secure
باعث میشود Cookie فقط روی اتصال HTTPS ارسال شود.
در Production:
Secure = true
باید جدی گرفته شود.
# 34. SameSite
SameSite به کنترل ارسال Cookie در درخواستهای Cross-Site کمک میکند.
مقادیر رایج:
Strict
Lax
None
اگر SameSite=None استفاده شود، معمولاً Secure نیز لازم است.
انتخاب مقدار مناسب باید بر اساس معماری Login، Domainها و Cross-Site Flow انجام شود.
# 35. Logout و Revocation
Logout فقط نباید Session محلی را پاک کند.
اگر Refresh Token داریم، بهتر است Token مربوطه نیز Revoked شود.
مثلاً:
UPDATE refresh_tokens
SET revoked_at = NOW()
WHERE id = ?
AND user_id = ?;
در نتیجه:
Logout
↓
Destroy Local Session
↓
Revoke Refresh Token
# 36. Logout از همه دستگاهها
فرض کنید کاربر روی سه دستگاه Login کرده است:
Laptop
Mobile
Tablet
و سه Refresh Token دارد:
Token A
Token B
Token C
اگر گزینهای مانند:
خروج از همه دستگاهها
داریم، میتوانیم تمام Tokenهای کاربر را Revoked کنیم:
UPDATE refresh_tokens
SET revoked_at = NOW()
WHERE user_id = ?
AND revoked_at IS NULL;
در نتیجه تمام Sessionهای مبتنی بر Refresh Token دیگر نمیتوانند Token جدید دریافت کنند.
# 37. Logout فقط از یک دستگاه
اگر کاربر فقط از Laptop خارج شود، بهتر است فقط Token مربوط به همان Device یا Session را Revoked کنیم.
به همین دلیل نگهداری Metadata مناسب برای Sessionها میتواند مفید باشد.
مثلاً:
device_name
created_at
last_used_at
ip_address
user_agent
البته ذخیره IP و User-Agent باید با نیاز واقعی سیستم و الزامات حریم خصوصی شما هماهنگ باشد.
# 38. Expiration برای Refresh Token
Refresh Token نباید تا ابد معتبر باشد.
مثلاً:
Refresh Token
Expiration:
30 days
یا بر اساس نیاز سیستم:
7 days
30 days
60 days
90 days
مقدار مناسب به مدل امنیتی و تجربه کاربری پروژه بستگی دارد.
# 39. Sliding Sessions
در بعضی سیستمها بعد از هر Refresh موفق، تاریخ انقضای Session تغییر میکند.
مثلاً:
Initial:
30 days
Refresh:
+30 days
به این مدل گاهی Sliding Session گفته میشود.
اما باید مراقب باشیم که Session عملاً بدون استفاده طولانیمدت هیچوقت منقضی نشود.
# 40. Absolute Expiration
یک راهکار بهتر در برخی معماریها این است که علاوه بر عمر هر Refresh Token، برای کل Session یک حداکثر عمر تعیین کنیم.
مثلاً:
Token lifetime:
7 days
Absolute session lifetime:
90 days
در نتیجه حتی اگر کاربر مرتب Refresh کند، بعد از 90 روز نیاز به احراز هویت مجدد خواهد داشت.
# 41. Rate Limiting برای Refresh Endpoint
Endpoint مربوط به Refresh نیز باید Rate Limit داشته باشد.
مثلاً:
POST /oauth/token
نباید بدون محدودیت در دسترس باشد.
میتوانیم محدودیتهایی مانند:
Per IP
Per Client
Per Token Family
داشته باشیم.
اگر تعداد درخواستهای ناموفق زیاد شد:
429 Too Many Requests
میتواند پاسخ مناسبی باشد.
# 42. Logging
برای بررسی مشکلات امنیتی باید Eventهای مهم را Log کنیم.
مثلاً:
refresh_success
refresh_failed
refresh_expired
refresh_revoked
refresh_reuse_detected
logout
اما نباید Raw Refresh Token را Log کنیم.
غلط:
Refresh token: abc123...
درستتر:
Refresh attempt failed
user_id=123
family_id=...
reason=expired
اطلاعات Log باید حداقل لازم را داشته باشد.
# 43. Refresh Token Reuse یک Event امنیتی مهم است
اگر Token Rotation فعال باشد و یک Refresh Token قبلاً استفادهشده دوباره ارائه شود، این وضعیت میتواند نشاندهنده استفاده همزمان از یک Credential قدیمی باشد.
مثلاً:
User:
A → B
Attacker:
A → ?
در این حالت میتوان Family را بررسی و در صورت طراحی سیستم، کل Family را باطل کرد.
این رفتار باید بهصورت مشخص در معماری سیستم تعریف شود و صرفاً بهعنوان یک خطای معمولی نادیده گرفته نشود.
# 44. معماری پیشنهادی کامل
یک معماری مناسب میتواند چنین باشد:
┌──────────────┐
│ User │
└──────┬───────┘
│
Login
│
▼
┌──────────────┐
│ Auth Server │
└──────┬───────┘
│
Access + Refresh Token
│
▼
┌──────────────┐
│ Client │
└──────┬───────┘
│
API Request
│
▼
┌──────────────┐
│ Resource API │
└──────────────┘
Access Token Expired
│
▼
Refresh Endpoint
│
▼
Validate Refresh Token
│
├── Invalid ──► 401
│
▼
Revoke Old Token
│
▼
Generate New Token
│
▼
Generate New Refresh Token
│
▼
Response
# 45. نمونه پاسخ Refresh موفق
میتوانیم پاسخ را به شکل زیر طراحی کنیم:
{
"token_type": "Bearer",
"access_token": "new-access-token",
"expires_in": 900,
"refresh_token": "new-refresh-token"
}
Client باید Refresh Token جدید را جایگزین Token قبلی کند.
# 46. نمونه Endpoint
در سادهترین حالت:
POST /oauth/token
Body:
grant_type=refresh_token
refresh_token=...
پاسخ موفق:
HTTP/1.1 200 OK
Content-Type: application/json
{
"token_type": "Bearer",
"access_token": "...",
"expires_in": 900,
"refresh_token": "..."
}
پاسخ Token نامعتبر:
HTTP/1.1 401 Unauthorized
# 47. ترتیب منطقی اعتبارسنجی
در Endpoint Refresh بهتر است فرآیند را به مراحل مشخص تقسیم کنیم:
1. دریافت Request
2. اعتبارسنجی grant_type
3. دریافت Refresh Token
4. Hash کردن Token
5. پیدا کردن Record
6. بررسی Revocation
7. بررسی Expiration
8. بررسی User
9. بررسی Client
10. بررسی Token Family
11. Rotation
12. صدور Access Token جدید
13. صدور Refresh Token جدید
14. ثبت Event
15. ارسال Response
این تفکیک باعث میشود Debug و نگهداری سیستم سادهتر شود.
# 48. Refresh Token و OAuth 2.0
در OAuth 2.0، Refresh Token یک نوع Credential برای دریافت Access Token جدید است.
اما نحوه دقیق صدور، نگهداری، Rotation و Revocation به معماری Authorization Server و Client بستگی دارد.
بنابراین نباید یک پیادهسازی ساده را بدون بررسی Threat Model به تمام پروژهها تعمیم دهیم.
# 49. Refresh Token و OpenID Connect
در OpenID Connect نیز ممکن است در بعضی Flowها Refresh Token دریافت شود.
اما نقش آن همچنان مربوط به دریافت Token جدید است.
ID Token و Refresh Token را نباید با یکدیگر اشتباه گرفت:
ID Token
→ اطلاعات هویت
Access Token
→ دسترسی به Resource/API
Refresh Token
→ دریافت Token جدید
# 50. آیا Refresh Token باید در JWT باشد؟
خیر.
یک Refresh Token تصادفی و Opaque در بسیاری از سیستمها انتخاب مناسبی است.
مثلاً:
$refreshToken = bin2hex(
random_bytes(32)
);
و:
$hash = hash(
'sha256',
$refreshToken
);
در دیتابیس ذخیره میشود.
این روش به Server اجازه میدهد بهصورت مستقیم وضعیت Token را کنترل کند.
# 51. نکته مهم درباره Hash کردن Token
Refresh Token معمولاً مقدار تصادفی و با Entropy بالا است.
بنابراین Hash کردن SHA-256 برای ذخیره Token تصادفی یک روش رایج است.
این موضوع با Password متفاوت است.
برای Password معمولاً از:
password_hash()
و الگوریتمهای مخصوص Password Hashing استفاده میکنیم.
برای یک Token تصادفی پرEntropy، SHA-256 میتواند برای ذخیره یک اثر انگشت Token استفاده شود.
# 52. پاکسازی Tokenهای قدیمی
اگر سیستم مدت زیادی کار کند، جدول Refresh Token میتواند بزرگ شود.
پس باید Job یا Cron برای پاکسازی Tokenهای قدیمی داشته باشیم.
مثلاً:
DELETE FROM refresh_tokens
WHERE expires_at < NOW()
AND revoked_at IS NOT NULL;
همچنین باید سیاست نگهداری Logها و Eventهای امنیتی را جداگانه مشخص کنیم.
# 53. ایندکسهای دیتابیس
برای سیستمهای بزرگ، Index مناسب مهم است.
مثلاً:
CREATE INDEX idx_refresh_token_user
ON refresh_tokens(user_id);
و:
CREATE INDEX idx_refresh_token_family
ON refresh_tokens(family_id);
همچنین:
UNIQUE(token_hash)
باعث میشود Token Hash تکراری ثبت نشود.
# 54. Race Condition در Rotation
یکی از نکات پیشرفته، Race Condition است.
فرض کنید Client به دلیل Retry دو درخواست Refresh تقریباً همزمان ارسال کند:
Request A ──┐
├── Refresh Token X
Request B ──┘
اگر Transaction و Lock مناسب نداشته باشیم، ممکن است هر دو درخواست Token X را معتبر ببینند.
بنابراین Rotation باید با Transaction و در صورت نیاز Row Locking طراحی شود.
مثلاً:
SELECT *
FROM refresh_tokens
WHERE token_hash = ?
FOR UPDATE;
و سپس Revocation و ایجاد Token جدید انجام شود.
# 55. معماری حرفهایتر
در یک سیستم بزرگتر میتوانیم Service جداگانه داشته باشیم:
AuthService
RefreshTokenService
TokenService
SessionService
UserIdentityService
مثلاً:
$tokens = $refreshTokenService->rotate(
$refreshToken
);
و Service مسئول موارد زیر باشد:
Validate
Lock
Revoke
Detect Reuse
Create New Token
Create Access Token
این ساختار از قرار دادن تمام منطق Authentication داخل Controller جلوگیری میکند.
# 56. Refresh Token در Laravel
اگر همین معماری را در Laravel پیاده کنیم، میتوانیم مدلهایی مانند:
User
RefreshToken
داشته باشیم.
و Service:
class RefreshTokenService
{
public function rotate(string $token)
{
// Validate
// Revoke
// Generate
// Return
}
}
همچنین میتوانیم از:
Database Transactions
Queues
Cache
Rate Limiting
Events
برای ساخت سیستم حرفهایتر استفاده کنیم.
# 57. چکلیست امنیت Refresh Token
قبل از Production این موارد را بررسی کنید:
☑ Refresh Token تصادفی و Cryptographically Secure است
☑ Raw Token در Database ذخیره نمیشود
☑ Token دارای Expiration است
☑ Token قابلیت Revocation دارد
☑ Token Rotation فعال است
☑ Reuse Detection در نظر گرفته شده
☑ Token Family در معماریهای حساس در نظر گرفته شده
☑ Access Token عمر کوتاهتری دارد
☑ Refresh Endpoint Rate Limit دارد
☑ HTTPS فعال است
☑ Refresh Token در Log ذخیره نمیشود
☑ Session Cookie امن است
☑ XSS و CSRF در معماری Browser بررسی شدهاند
☑ Logout باعث Revocation میشود
☑ Logout All قابل پیادهسازی است
☑ Race Condition در Rotation بررسی شده
☑ Database Transaction استفاده شده
☑ Tokenهای منقضی پاکسازی میشوند
☑ Client و User در صورت نیاز اعتبارسنجی میشوند
# 58. اشتباهات رایج
اشتباه ۱: عمر بسیار طولانی Access Token
Access Token = 1 year
میتواند در صورت سرقت، پنجره سوءاستفاده بزرگی ایجاد کند.
اشتباه ۲: استفاده دائمی از یک Refresh Token
اگر هر بار همان Token را استفاده کنیم:
Refresh Token A
↓
A
↓
A
↓
A
کنترل سرقت و Reuse دشوارتر میشود.
اشتباه ۳: ذخیره Raw Token
refresh_tokens.token = RAW_TOKEN
رویکرد مناسبی برای Storage امن نیست.
اشتباه ۴: قرار دادن Refresh Token در URL
مثلاً:
https://example.com/refresh?token=...
Credential حساس نباید در URL قرار بگیرد، چون URL ممکن است در History، Logها یا سایر مکانیزمها ثبت شود.
اشتباه ۵: نبود Revocation
اگر Token را نتوانیم باطل کنیم، Logout و پاسخ به برخی رخدادهای امنیتی دشوارتر میشود.
اشتباه ۶: نبود Rate Limiting
Refresh Endpoint نیز یک Endpoint امنیتی است و نباید بدون محدودیت رها شود.
# 59. پرسشهای متداول
آیا Refresh Token از Access Token مهمتر است؟
از نظر مدیریت امنیتی، Refresh Token معمولاً حساسیت بسیار بالایی دارد، چون میتواند برای دریافت Access Tokenهای جدید استفاده شود.
آیا Refresh Token باید JWT باشد؟
خیر.
میتواند یک Token تصادفی و Opaque باشد.
آیا Refresh Token را میتوان در localStorage ذخیره کرد؟
از نظر فنی امکانپذیر است، اما در Applicationهای Browser-based باید ریسک XSS و سایر Threatها را در نظر گرفت. برای Credentialهای حساس، انتخاب Storage باید بخشی از طراحی امنیتی سیستم باشد.
آیا با Logout باید Refresh Token را حذف کنیم؟
بهتر است آن را Revoked کنیم یا وضعیت غیرقابلاستفاده برای آن ثبت کنیم. حذف فیزیکی نیز در بعضی معماریها ممکن است، اما داشتن سابقه Revocation میتواند برای Audit و تشخیص Reuse مفید باشد.
آیا Refresh Token باید یک سال اعتبار داشته باشد؟
عدد ثابت و مناسبی برای همه پروژهها وجود ندارد. مدت اعتبار باید بر اساس نوع Application، سطح ریسک، تجربه کاربری و سیاست امنیتی تعیین شود.
آیا Rotation اجباری است؟
برای همه معماریها نمیتوان یک پاسخ یکسان داد، اما Rotation و Reuse Detection میتوانند در کاهش ریسک استفاده مجدد از Refresh Tokenهای سرقتشده نقش مهمی داشته باشند.
# جمعبندی
Refresh Token یکی از بخشهای مهم سیستمهای Authentication مبتنی بر OAuth 2.0 است.
معماری پایه:
Login
↓
Access Token + Refresh Token
↓
Access Token Expired
↓
Refresh Token
↓
Validate
↓
Rotate
↓
New Access Token
+
New Refresh Token
اما یک سیستم Production نباید فقط به صدور Token جدید فکر کند.
مواردی مانند:
Token Rotation
Token Revocation
Token Family
Reuse Detection
Expiration
Rate Limiting
Secure Storage
Transaction
Race Condition
Session Security
همگی باید در طراحی نهایی در نظر گرفته شوند.
یکی از مهمترین اصول این است:
> Refresh Token را مانند یک Credential بسیار حساس در نظر بگیرید، نه یک رشته ساده برای نگهداری در دیتابیس.
اگر این معماری درست طراحی شود، میتوانیم Access Tokenهای کوتاهعمر داشته باشیم و در عین حال کاربر را مجبور نکنیم مرتباً Login کند.
مقاله بعدی
بعد از یادگیری Refresh Token، مرحله بعدی میتواند ساخت یک OAuth 2.0 Authorization Server در PHP باشد؛ یعنی بهجای اینکه PHP فقط به Google یا یک Provider خارجی متصل شود، خودمان یک Server بسازیم که Clientها بتوانند از آن Authorization Code، Access Token و Refresh Token دریافت کنند.
این موضوع میتواند یکی از مهمترین مقالات بخش OAuth و API Security در PHP باشد و مباحثی مثل Client، Scope، Authorization Code، PKCE، Token Endpoint، Consent و Revocation را در یک پروژه واقعی کنار هم قرار دهد.





