# مدیریت Access Token و Refresh Token در OpenID Connect با PHP؛ ذخیرهسازی، انقضا، Rotation و Revocation
در معماریهای مدرن احراز هویت، فقط ورود موفق کاربر اهمیت ندارد؛ بلکه مدیریت صحیح Tokenها بعد از ورود نیز بخش بسیار مهمی از امنیت سیستم است.
فرض کنید کاربر در یک سایت PHP با استفاده از OpenID Connect وارد شده است. Identity Provider بعد از تأیید هویت، معمولاً مجموعهای از Tokenها را در اختیار برنامه قرار میدهد:
- ID Token
- Access Token
- Refresh Token
هرکدام از این Tokenها کاربرد متفاوتی دارند و نباید با یکدیگر اشتباه گرفته شوند.
یکی از اشتباهات رایج در پیادهسازی OAuth 2.0 و OpenID Connect این است که توسعهدهنده Access Token را برای مدت بسیار طولانی معتبر نگه میدارد، Refresh Token را در محل ناامن ذخیره میکند یا اصلاً مکانیزم Rotation و Revocation برای آن در نظر نمیگیرد.
در این مقاله یاد میگیریم چگونه در یک پروژه PHP، چرخه عمر Access Token و Refresh Token را به شکل اصولی مدیریت کنیم.
Access Token، Refresh Token و ID Token چه تفاوتی دارند؟
قبل از پیادهسازی باید نقش هر Token را بهخوبی بدانیم.
ID Token
ID Token مربوط به هویت کاربر است.
در OpenID Connect معمولاً اطلاعاتی مانند این موارد در آن وجود دارد:
{
"iss": "https://auth.example.com",
"sub": "123456",
"aud": "client-123",
"exp": 1790000000,
"iat": 1789999000,
"email": "user@example.com"
}
این Token به Client میگوید:
> کاربری که احراز هویت شده چه کسی است؟
بنابراین ID Token را نباید به عنوان Token دسترسی به API در نظر گرفت.
Access Token چیست؟
Access Token برای دسترسی به Resource Server یا API استفاده میشود.
مثلاً:
Authorization: Bearer ACCESS_TOKEN
API با استفاده از Access Token بررسی میکند که درخواستکننده چه دسترسیهایی دارد.
Access Token معمولاً ویژگیهایی مانند موارد زیر دارد:
isssubaudscopeexpiat
اگر Access Token از نوع JWT باشد، Resource Server میتواند Signature و Claimهای آن را بررسی کند.
Refresh Token چیست؟
Refresh Token برای گرفتن Access Token جدید استفاده میشود.
به عنوان مثال:
Access Token
↓
منقضی میشود
↓
Refresh Token
↓
Access Token جدید
نکته مهم این است که Refresh Token معمولاً نباید برای دسترسی مستقیم به API استفاده شود.
یعنی این کار اشتباه است:
GET /api/profile
Authorization: Bearer REFRESH_TOKEN
Refresh Token برای Endpoint مربوط به Token استفاده میشود، نه Resource API.
# چرخه کامل Token در OpenID Connect
یک جریان ساده میتواند به این شکل باشد:
User
│
▼
Application
│
│ Authorization Code
▼
Identity Provider
│
├── ID Token
├── Access Token
└── Refresh Token
│
▼
Application
│
▼
Resource API
بعد از مدتی:
Access Token
│
▼
Expired
│
▼
Refresh Token
│
▼
New Access Token
│
└── New Refresh Token
اگر Refresh Token Rotation فعال باشد، Refresh Token قبلی نیز دیگر نباید قابل استفاده باشد.
# چرا Access Token باید کوتاهعمر باشد؟
Access Token معمولاً یک Bearer Credential است.
یعنی اگر مهاجم آن را به دست بیاورد، ممکن است بتواند تا زمان اعتبار Token از آن استفاده کند.
فرض کنید Access Token شما 30 روز اعتبار داشته باشد.
اگر این Token دزدیده شود، مهاجم ممکن است برای مدت بسیار طولانی امکان استفاده از آن را داشته باشد.
به همین دلیل معمولاً بهتر است Access Token عمر نسبتاً کوتاهی داشته باشد.
مثلاً:
Access Token Lifetime = 10 minutes
یا:
Access Token Lifetime = 15 minutes
این اعداد قانون ثابت نیستند و باید بر اساس معماری، سطح حساسیت API و نیازهای پروژه انتخاب شوند.
# Refresh Token چرا حساستر است؟
Refresh Token معمولاً عمر بیشتری دارد.
مثلاً:
Access Token
10 minutes
Refresh Token
30 days
بنابراین Refresh Token از نظر امنیتی بسیار حساس است.
اگر مهاجم Refresh Token را به دست بیاورد، ممکن است بتواند Access Tokenهای جدید دریافت کند.
به همین دلیل:
> Refresh Token باید حتی با حساسیت بیشتری نسبت به Access Token محافظت شود.
# Refresh Token را کجا ذخیره کنیم؟
محل ذخیرهسازی Token یکی از مهمترین تصمیمات امنیتی است.
در یک PHP Application سمت سرور، ترجیح مناسب این است که Tokenها تا حد امکان در سمت سرور نگهداری شوند.
برای مثال:
Browser
│
│ Session Cookie
▼
PHP Application
│
├── Access Token
└── Refresh Token
در این معماری Browser مستقیماً Refresh Token را نگهداری نمیکند.
# چرا localStorage گزینه مناسبی برای Refresh Token نیست؟
یکی از پیادهسازیهای رایج این است:
localStorage.setItem(
'refresh_token',
refreshToken
);
این روش در برابر XSS میتواند مشکلساز شود.
اگر کد مخرب بتواند به JavaScript صفحه دسترسی پیدا کند، ممکن است بتواند Token را نیز بخواند.
بنابراین در برنامههای Server-Side PHP معمولاً بهتر است Refresh Token را در سمت سرور نگهداری کنیم.
# ساختار پیشنهادی Database
برای مدیریت Refresh Token میتوانیم جدولی مانند زیر داشته باشیم:
CREATE TABLE oidc_refresh_tokens (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
client_id VARCHAR(255) NOT NULL,
token_hash VARCHAR(255) NOT NULL,
family_id CHAR(36) NOT NULL,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NULL,
replaced_by BIGINT UNSIGNED NULL,
created_at DATETIME NOT NULL,
last_used_at DATETIME NULL,
INDEX idx_user_id (user_id),
INDEX idx_client_id (client_id),
INDEX idx_family_id (family_id),
INDEX idx_expires_at (expires_at)
);
در پروژه واقعی میتوان ستونهای بیشتری نیز اضافه کرد.
مثلاً:
device_name
ip_address
user_agent
session_id
البته ذخیره IP و User-Agent باید با توجه به نیاز، حریم خصوصی و سیاست نگهداری داده انجام شود.
# چرا باید family_id داشته باشیم؟
فرض کنید کاربر روی سه دستگاه وارد شده است:
Laptop
└── Refresh Token Family A
Mobile
└── Refresh Token Family B
Tablet
└── Refresh Token Family C
اگر Refresh Token دستگاه موبایل مشکوک شود، میتوانیم فقط Family مربوط به همان Session را Revoke کنیم.
این معماری مدیریت Sessionهای متعدد را بسیار سادهتر میکند.
# Refresh Token Rotation چیست؟
Refresh Token Rotation یعنی هر بار که Refresh Token برای دریافت Token جدید استفاده میشود، Refresh Token قبلی باطل شود.
مثلاً:
RT-001
│
▼
Refresh
│
├── AT-002
└── RT-002
بعد:
RT-001 = Revoked
RT-002 = Active
در Refresh بعدی:
RT-002
│
▼
Refresh
│
├── AT-003
└── RT-003
و:
RT-002 = Revoked
RT-003 = Active
این کار باعث میشود یک Refresh Token قدیمی دائماً قابل استفاده نباشد.
# Token Reuse چیست؟
فرض کنید کاربر ابتدا از:
RT-001
استفاده کرده و سیستم آن را باطل کرده است.
سپس:
RT-002
ساخته شده است.
حالا شخص دیگری تلاش کند دوباره از:
RT-001
استفاده کند.
این اتفاق میتواند نشانهای از سرقت Token باشد.
به این وضعیت:
Refresh Token Reuse
گفته میشود.
# هنگام تشخیص Token Reuse چه کنیم؟
یکی از روشهای متداول این است که کل Token Family را Revoke کنیم.
مثلاً:
Family A
RT-001 ❌
RT-002 ❌
RT-003 ❌
این کار باعث میشود مهاجمی که یک Token قدیمی را در اختیار دارد نتواند از زنجیره Token استفاده کند.
همچنین میتوان یک Event امنیتی ثبت کرد:
refresh_token_reuse_detected
و اطلاعات مناسب برای بررسی امنیتی را در Audit Log نگهداری کرد.
نباید خود Token خام را در Log ذخیره کنیم.
# Token Hash یا Token Encryption؟
دو رویکرد اصلی برای ذخیره Refresh Token وجود دارد.
روش اول: ذخیره Hash
در این روش Token اصلی را ذخیره نمیکنیم.
مثلاً:
$hash = hash(
'sha256',
$refreshToken
);
سپس Hash را در Database ذخیره میکنیم.
هنگام دریافت Token:
$hash = hash(
'sha256',
$incomingToken
);
و Hash را با مقدار Database مقایسه میکنیم.
برای Tokenهایی که به شکل تصادفی و با Entropy بالا تولید شدهاند، این روش میتواند مناسب باشد.
# چرا Token باید تصادفی باشد؟
نباید Refresh Token را به شکل قابل حدس تولید کنیم.
این اشتباه است:
$token = uniqid();
یا:
$token = $userId . '-' . time();
به جای آن از CSPRNG استفاده کنیم:
$token = bin2hex(random_bytes(32));
در این حالت یک Token تصادفی و بسیار دشوار برای حدسزدن تولید میشود.
مثلاً:
a8f2c7...
# روش دوم: ذخیره رمزنگاریشده
اگر معماری شما نیاز داشته باشد Token اصلی بعداً بازیابی شود، میتوان آن را به صورت رمزنگاریشده نگهداری کرد.
در این مدل:
Refresh Token
↓
Encryption
↓
Database
و هنگام استفاده:
Database
↓
Decrypt
↓
Refresh Token
کلید رمزنگاری باید خارج از Database و در Secret Management یا Environment امن نگهداری شود.
# Refresh Token Endpoint چگونه کار میکند؟
یک درخواست معمولی میتواند شبیه این باشد:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=...
Identity Provider یا Authorization Server باید مراحل مختلفی را بررسی کند.
# مراحل اعتبارسنجی Refresh Token
به صورت ساده:
Receive Refresh Token
│
▼
Find Token
│
▼
Does it exist?
│
▼
Is it revoked?
│
▼
Is it expired?
│
▼
Is client valid?
│
▼
Is token family valid?
│
▼
Rotate Token
│
▼
Issue New Tokens
# پیادهسازی ساده Refresh Token در PHP
فرض کنیم Token ورودی را دریافت کردهایم:
$refreshToken = $_POST['refresh_token'] ?? null;
if (!$refreshToken) {
http_response_code(400);
echo json_encode([
'error' => 'invalid_request'
]);
exit;
}
Hash آن را محاسبه میکنیم:
$tokenHash = hash(
'sha256',
$refreshToken
);
سپس Token را از Database پیدا میکنیم:
$stmt = $pdo->prepare(
'SELECT *
FROM oidc_refresh_tokens
WHERE token_hash = :hash
LIMIT 1'
);
$stmt->execute([
'hash' => $tokenHash
]);
$token = $stmt->fetch(PDO::FETCH_ASSOC);
اگر پیدا نشد:
if (!$token) {
http_response_code(401);
echo json_encode([
'error' => 'invalid_grant'
]);
exit;
}
# بررسی Expiration
فرض کنیم:
$expiresAt = new DateTimeImmutable(
$token['expires_at']
);
$now = new DateTimeImmutable();
سپس:
if ($expiresAt <= $now) {
http_response_code(401);
echo json_encode([
'error' => 'invalid_grant'
]);
exit;
}
# بررسی Revocation
اگر:
$token['revoked_at'] !== null
باشد، Token دیگر قابل استفاده نیست.
if ($token['revoked_at'] !== null) {
// احتمال Token Reuse
}
در این نقطه میتوانیم بسته به وضعیت Token و سیاست امنیتی سیستم، Reuse Detection را اجرا کنیم.
# Rotation در یک Transaction
Rotation باید تا حد امکان Atomic باشد.
یعنی این عملیات نباید در شرایط Race Condition به صورت ناقص اجرا شود.
مثلاً:
$pdo->beginTransaction();
try {
// Lock current token
// Revoke current token
// Create new refresh token
// Create new access token
// Commit
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
# چرا Lock مهم است؟
فرض کنید دو درخواست تقریباً همزمان Refresh Token را ارسال کنند:
Request A ── RT-001
Request B ── RT-001
اگر هیچ Lock یا کنترل همزمانی نداشته باشیم، ممکن است هر دو درخواست تصور کنند Token معتبر است.
بنابراین برای عملیات حساس Token بهتر است از Transaction و Row Lock مناسب Database استفاده شود.
در MySQL میتوان از:
SELECT *
FROM oidc_refresh_tokens
WHERE id = ?
FOR UPDATE
استفاده کرد.
# ایجاد Refresh Token جدید
بعد از اعتبارسنجی:
$newRefreshToken = bin2hex(
random_bytes(32)
);
Hash آن:
$newHash = hash(
'sha256',
$newRefreshToken
);
سپس Token قبلی را باطل میکنیم:
UPDATE oidc_refresh_tokens
SET revoked_at = NOW()
WHERE id = ?
و Token جدید را ثبت میکنیم.
# Access Token جدید
در صورتی که Access Token از نوع JWT باشد، میتوانیم آن را با اطلاعات موردنیاز ایجاد کنیم.
برای مثال:
$claims = [
'iss' => 'https://auth.example.com',
'sub' => (string) $userId,
'aud' => 'api.example.com',
'iat' => time(),
'exp' => time() + 900,
'scope' => 'profile email'
];
در این مثال:
900 seconds = 15 minutes
است.
# Refresh Token را به API ارسال نکنید
فرض کنید برنامه شما API زیر را دارد:
GET /api/orders
باید Access Token ارسال شود:
Authorization: Bearer ACCESS_TOKEN
نه:
Authorization: Bearer REFRESH_TOKEN
Refresh Token باید فقط در مسیر Token Management استفاده شود.
# چه زمانی Access Token را Refresh کنیم؟
فرض کنید:
Access Token:
10:00 → 10:15
لازم نیست دقیقاً در:
10:15
منتظر بمانیم.
برنامه میتواند کمی قبل از انقضا Refresh کند.
مثلاً:
10:13
این کار باعث کاهش احتمال خطای درخواستهای همزمان میشود.
اما نباید هر Request بدون توجه به Expiration باعث Refresh شود.
# Refresh Storm چیست؟
فرض کنید یک برنامه 20 درخواست همزمان ارسال کند و Access Token منقضی شده باشد.
اگر هر Request جداگانه Refresh کند:
Request 1 → Refresh
Request 2 → Refresh
Request 3 → Refresh
...
Request 20 → Refresh
مشکل ایجاد میشود.
خصوصاً اگر Rotation فعال باشد.
به این وضعیت معمولاً Refresh Storm گفته میشود.
# جلوگیری از Refresh Storm
میتوان از یک Lock یا Single-Flight Mechanism استفاده کرد.
مثلاً:
Request A
│
├── Refresh
│
Request B ── wait
Request C ── wait
Request D ── wait
بعد از ایجاد Token جدید:
New Access Token
│
├── Request B
├── Request C
└── Request D
در سیستمهای بزرگتر میتوان از Redis یا Lockهای توزیعشده استفاده کرد.
# Revocation چیست؟
Revocation یعنی یک Token قبل از پایان زمان طبیعی اعتبارش، عمداً باطل شود.
مثلاً کاربر:
- Logout کند
- رمز عبور خود را تغییر دهد
- حسابش غیرفعال شود
- دستگاهی را از حساب حذف کند
- فعالیت مشکوک داشته باشد
در این شرایط میتوان Refresh Token را Revoke کرد.
# Logout و Token Revocation
Logout در معماری OIDC چند بخش دارد.
مثلاً:
Browser
│
▼
Destroy Local Session
│
▼
Revoke Refresh Token
│
▼
OIDC Logout
│
▼
Identity Provider
این موضوع در مقاله قبلی درباره RP-Initiated Logout نیز اهمیت داشت.
صرفاً حذف Session محلی همیشه به معنای پایان کامل Session در Identity Provider نیست.
# تغییر رمز عبور کاربر
فرض کنید کاربر رمز عبور خود را تغییر داده است.
بسته به سیاست امنیتی سیستم، میتوان تمام Refresh Tokenهای فعال آن کاربر را Revoke کرد:
UPDATE oidc_refresh_tokens
SET revoked_at = NOW()
WHERE user_id = ?
AND revoked_at IS NULL;
در نتیجه Sessionهای قدیمی مجبور به احراز هویت مجدد میشوند.
# غیرفعال شدن حساب کاربر
اگر حساب کاربر Suspend یا Disabled شود، بهتر است Refresh Tokenهای فعال او نیز دیگر قابل استفاده نباشند.
مثلاً:
User Disabled
│
▼
Revoke Active Refresh Tokens
│
▼
Existing Access Tokens
│
▼
Expire naturally / optional server-side invalidation
رفتار Access Tokenهای قبلی به معماری سیستم بستگی دارد.
اگر Access Token یک JWT کاملاً Stateless باشد، Revoke کردن آن قبل از exp نیازمند سازوکار اضافهای است.
# JWT Access Token یا Opaque Access Token؟
Access Token میتواند JWT باشد یا Opaque.
JWT
مثلاً:
eyJhbGciOiJSUzI1NiIs...
Resource Server میتواند Signature را بررسی کند.
مزیت:
Local Validation
اما Revocation فوری آن دشوارتر است.
Opaque Token
مثلاً:
7c9d4a1f...
Resource Server نمیتواند از محتوای Token به شکل مستقل اطلاعات را استخراج کند.
ممکن است برای اعتبارسنجی به Authorization Server یا Introspection Endpoint مراجعه کند.
در این مدل کنترل مرکزی بیشتری دارید، اما هزینه ارتباط و وابستگی بیشتر میشود.
# Token Introspection چیست؟
در معماریهایی که از Opaque Token استفاده میکنند، Resource Server میتواند وضعیت Token را از Authorization Server بپرسد.
مثلاً:
POST /oauth/introspect
و نتیجهای شبیه این دریافت کند:
{
"active": true,
"client_id": "client-123",
"sub": "123456",
"scope": "profile email",
"exp": 1790000000
}
در نتیجه Resource Server میتواند متوجه شود Token هنوز فعال است یا خیر.
# Tokenها را Log نکنید
این کار بسیار خطرناک است:
Log::info(
'Access token: ' . $accessToken
);
یا:
Log::info([
'refresh_token' => $refreshToken
]);
Logها ممکن است توسط افراد یا سرویسهای مختلف قابل دسترسی باشند.
به جای آن:
Log::info('Token refresh completed', [
'user_id' => $userId,
'client_id' => $clientId,
]);
اطلاعات لازم برای Debug را نگه دارید، نه Credential اصلی را.
# Token را داخل URL قرار ندهید
این کار مناسب نیست:
https://example.com/callback?access_token=...
Token موجود در URL ممکن است در موارد مختلفی باقی بماند، از جمله:
- Browser History
- Access Logs
- Proxy Logs
- Analytics
- Referer
برای همین Token باید از مکانیزم مناسب انتقال داده استفاده شود.
# Access Token و Refresh Token در PHP Session
در یک برنامه Server-Side PHP میتوان Session را به عنوان لایهای بین Browser و Tokenها قرار داد.
مثلاً:
$_SESSION['user_id'] = $userId;
$_SESSION['authenticated'] = true;
و Tokenها در سمت سرور نگهداری شوند.
معماری:
Browser
│
│ Secure Session Cookie
▼
PHP Application
│
├── User Session
│
└── Token Storage
│
├── Access Token
└── Refresh Token
این مدل باعث میشود Browser مستقیماً با Refresh Token کار نکند.
# Secure Cookie
اگر Session با Cookie مدیریت میشود، تنظیمات امنیتی آن اهمیت زیادی دارد.
مثلاً:
session_set_cookie_params([
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
در محیط Production باید HTTPS فعال باشد.
# یک معماری پیشنهادی برای پروژه PHP
برای پروژهای که چند سایت و API دارد میتوان ساختاری مانند زیر در نظر گرفت:
auth.example.com
│
▼
Identity Provider
│
├── Authorization Server
├── Token Endpoint
├── JWKS Endpoint
└── Logout Endpoint
│
▼
Token Database
│
├── Access Token
└── Refresh Token
و Clientها:
portal.example.com
shop.example.com
admin.example.com
api.example.com
هر Client میتواند Session محلی خودش را داشته باشد، در حالی که احراز هویت مرکزی در Identity Provider انجام میشود.
# ساختار پیشنهادی سرویس Token در PHP
بهتر است تمام منطق Token را در Controller قرار ندهیم.
مثلاً:
app/
├── Services/
│ ├── TokenService.php
│ ├── RefreshTokenService.php
│ └── TokenRevocationService.php
│
├── Repositories/
│ └── RefreshTokenRepository.php
│
└── Controllers/
└── TokenController.php
برای مثال:
class RefreshTokenService
{
public function refresh(string $refreshToken): array
{
// Validate token
// Detect reuse
// Rotate token
// Issue access token
// Return result
}
}
این ساختار در پروژههای بزرگتر قابل نگهداریتر است.
# برای هر دستگاه یک Session جدا
بهتر است همه دستگاهها یک Refresh Token مشترک نداشته باشند.
مثلاً:
User
├── Chrome Desktop
│ └── Family A
│
├── Android
│ └── Family B
│
└── iPhone
└── Family C
در این صورت کاربر میتواند فقط دستگاه Android را Logout کند.
# مدیریت دستگاههای فعال
در پنل کاربر میتوان چیزی شبیه این نمایش داد:
دستگاههای فعال
Chrome - Windows
آخرین فعالیت: امروز
Android
آخرین فعالیت: دیروز
iPhone
آخرین فعالیت: 3 روز قبل
و کنار هرکدام:
خروج از این دستگاه
با کلیک روی آن، Refresh Token Family همان Session را Revoke میکنیم.
# هنگام Logout چه چیزهایی را انجام دهیم؟
یک Logout کامل میتواند شامل این مراحل باشد:
1. Destroy Local Session
2. Revoke Refresh Token
3. Revoke Token Family
4. Clear Authentication Cookies
5. OIDC Provider Logout
6. Redirect User
البته اجرای همه این مراحل به معماری پروژه و نوع Logout شما بستگی دارد.
# اشتباهات رایج در مدیریت Token
1. Access Token بسیار طولانی
مثلاً:
Access Token = 30 days
در بسیاری از معماریها چنین عمر طولانیای سطح ریسک Credential دزدیدهشده را افزایش میدهد.
2. ذخیره Refresh Token در localStorage
در برنامههای Server-Side معمولاً بهتر است Token حساس را در سمت سرور نگهداری کنیم.
3. ارسال Refresh Token به API
Refresh Token برای Token Endpoint است، نه Resource API.
4. عدم استفاده از Rotation
اگر یک Refresh Token مدت طولانی قابل استفاده باشد، در صورت سرقت، مهاجم فرصت بیشتری خواهد داشت.
5. نداشتن Reuse Detection
اگر Refresh Token قدیمی دوباره استفاده شد، باید بتوانیم آن را تشخیص دهیم.
6. نداشتن Revocation
سیستم باید بتواند Token را قبل از Expiration نیز باطل کند.
7. ذخیره Token خام در Log
Token را Credential در نظر بگیرید.
Log کردن آن میتواند یک مشکل امنیتی جدی ایجاد کند.
8. استفاده از Token در URL
Token نباید در Query String قرار گیرد.
9. نادیده گرفتن Race Condition
Rotation بدون Transaction و Lock مناسب میتواند در درخواستهای همزمان مشکل ایجاد کند.
10. استفاده از یک Refresh Token برای تمام دستگاهها
برای مدیریت Sessionهای جداگانه، بهتر است Token Familyهای مستقل داشته باشیم.
# چکلیست امنیت Access Token و Refresh Token
قبل از انتشار سیستم در Production بررسی کنید:
- [ ] Access Token عمر محدودی دارد.
- [ ] Refresh Token عمر مناسب دارد.
- [ ] Refresh Token به API ارسال نمیشود.
- [ ] Refresh Token در محل امن نگهداری میشود.
- [ ] Token خام در Log ثبت نمیشود.
- [ ] Refresh Token دارای Entropy کافی است.
- [ ] Refresh Token Rotation پیادهسازی شده است.
- [ ] Token Reuse قابل تشخیص است.
- [ ] Token Family قابل Revoke است.
- [ ] Logout باعث مدیریت مناسب Tokenها میشود.
- [ ] تغییر رمز عبور سیاست مشخصی برای Sessionهای قبلی دارد.
- [ ] حساب غیرفعالشده نمیتواند Token جدید بگیرد.
- [ ] عملیات Refresh دارای Transaction مناسب است.
- [ ] Race Condition کنترل شده است.
- [ ] Token در URL قرار نمیگیرد.
- [ ] Cookieهای Session دارای تنظیمات امنیتی مناسب هستند.
- [ ] HTTPS در Production فعال است.
- [ ] Access Token و Refresh Token با یکدیگر اشتباه گرفته نمیشوند.
# پرسشهای متداول
آیا Access Token و Refresh Token یکی هستند؟
خیر.
Access Token برای دسترسی به Resource Server استفاده میشود و Refresh Token برای دریافت Token جدید.
آیا Refresh Token را میتوان به API فرستاد؟
خیر، در معماری استاندارد نباید Refresh Token را به Resource API ارسال کنید.
آیا Access Token باید JWT باشد؟
خیر.
Access Token میتواند JWT یا Opaque Token باشد.
انتخاب آن به معماری سیستم بستگی دارد.
Refresh Token چقدر باید اعتبار داشته باشد؟
عدد ثابتی وجود ندارد.
به نوع برنامه، حساسیت دادهها، سیاست Session و نیازهای امنیتی بستگی دارد.
آیا باید Refresh Token Rotation داشته باشیم؟
در بسیاری از معماریهایی که Refresh Token قابل استفاده مجدد هستند، Rotation میتواند یک لایه امنیتی مهم باشد؛ خصوصاً زمانی که همراه با Reuse Detection پیادهسازی شود.
اگر Refresh Token دزدیده شود چه اتفاقی میافتد؟
بسته به معماری، مهاجم ممکن است بتواند Access Token جدید دریافت کند.
به همین دلیل باید Refresh Token را به شکل امن ذخیره کرد و Rotation، Reuse Detection و Revocation را در نظر گرفت.
هنگام Logout فقط Session را حذف کنیم؟
اگر برنامه از OIDC و Refresh Token استفاده میکند، حذف Session محلی لزوماً به معنی باطل شدن Tokenهای سمت Identity Provider نیست.
بنابراین Logout باید متناسب با معماری Token و Identity Provider طراحی شود.
# جمعبندی
مدیریت Tokenها یکی از مهمترین بخشهای پیادهسازی امن OAuth 2.0 و OpenID Connect در PHP است.
سه مفهوم اصلی را همیشه از یکدیگر جدا نگه دارید:
ID Token
→ هویت کاربر
Access Token
→ دسترسی به API
Refresh Token
→ دریافت Access Token جدید
برای یک پیادهسازی حرفهای نیز بهتر است این موارد را در کنار هم داشته باشید:
Short-lived Access Token
+
Secure Refresh Token Storage
+
Refresh Token Rotation
+
Reuse Detection
+
Token Revocation
+
Transaction / Locking
+
Secure Logout
در چنین معماریای، Token فقط یک رشته تصادفی نیست؛ بلکه بخشی از چرخه مدیریت هویت و Session کاربر محسوب میشود.
این موضوع خصوصاً در پروژههایی که یک Identity Provider مرکزی و چند Application دارند اهمیت بیشتری پیدا میکند.





