در مقاله قبلی درباره مدیریت Access Token و Refresh Token در OpenID Connect با PHP یاد گرفتیم که Tokenها باید چرخه عمر مشخصی داشته باشند.
Access Token معمولاً کوتاهعمر است و Refresh Token میتواند برای مدت بیشتری معتبر باشد. همچنین درباره Refresh Token Rotation، تشخیص Token Reuse، Logout و مدیریت Sessionهای مختلف صحبت کردیم.
اما یک سؤال مهم باقی میماند:
اگر بخواهیم یک Token را قبل از زمان Expiration آن باطل کنیم چه کار باید کنیم؟
یا:
اگر یک API بخواهد بداند یک Access Token در حال حاضر فعال است یا خیر، چگونه باید این موضوع را بررسی کند؟
اینجاست که دو مفهوم مهم وارد معماری OAuth 2.0 میشوند:
- Token Revocation
- Token Introspection
این دو مفهوم شبیه به هم به نظر میرسند، اما کاربرد متفاوتی دارند.
در این مقاله با PHP یاد میگیریم چگونه این دو مکانیزم را طراحی و پیادهسازی کنیم و چه زمانی استفاده از هرکدام منطقیتر است.
# Token Revocation چیست؟
Token Revocation یعنی یک Token را قبل از پایان زمان طبیعی اعتبارش عمداً باطل کنیم.
فرض کنید Access Token شما:
10:00 → 10:15
معتبر است.
اگر کاربر در ساعت:
10:05
Logout کند، از نظر زمانی Token هنوز 10 دقیقه دیگر اعتبار دارد.
اما سیستم میتواند آن را Revoke کند.
در نتیجه:
10:00
│
▼
Access Token
│
10:05
│
▼
REVOKED
پس:
> Expiration و Revocation یک مفهوم نیستند.
# تفاوت Expiration و Revocation
Expiration
Token خودش به صورت طبیعی منقضی میشود.
Token
│
├── 10:00 Created
│
└── 10:15 Expired
Revocation
سیستم قبل از Expiration تصمیم میگیرد Token را باطل کند.
Token
│
├── 10:00 Created
│
└── 10:05 Revoked
این تفاوت در طراحی سیستمهای Authentication بسیار مهم است.
# چه زمانی Token را Revoke کنیم؟
سناریوهای مختلفی وجود دارد.
Logout
کاربر از حساب خود خارج میشود:
Logout
↓
Revoke Refresh Token
↓
Destroy Local Session
تغییر رمز عبور
مثلاً کاربر رمز عبور خود را تغییر میدهد.
بر اساس سیاست امنیتی سیستم میتوان Sessionهای قبلی را نیز Revoke کرد.
Password Changed
↓
Revoke Active Sessions
↓
Require Login
غیرفعال شدن حساب
اگر مدیر حساب کاربر را غیرفعال کند:
User Disabled
↓
Revoke Active Refresh Tokens
سرقت Token
اگر سیستم متوجه استفاده مجدد از Refresh Token قدیمی شود:
Refresh Token Reuse
↓
Security Event
↓
Revoke Token Family
# Token Introspection چیست؟
Introspection یعنی یک Resource Server از Authorization Server میپرسد:
> آیا این Token هنوز فعال است و چه اطلاعاتی درباره آن وجود دارد؟
مثلاً API یک Access Token دریافت کرده است:
API
│
│ Access Token
▼
Authorization Server
│
│ Introspection
▼
Token Status
پاسخ میتواند چیزی شبیه این باشد:
{
"active": true,
"client_id": "client-123",
"username": "user123",
"scope": "profile email",
"sub": "1001",
"aud": "api.example.com",
"iss": "https://auth.example.com",
"exp": 1790000000,
"iat": 1789999000
}
مهمترین فیلد:
active
است.
# تفاوت Revocation و Introspection
این دو مفهوم را نباید با یکدیگر اشتباه گرفت.
| مفهوم | کاربرد |
|---|---|
| Revocation | باطل کردن Token |
| Introspection | بررسی وضعیت Token |
| Expiration | پایان اعتبار طبیعی Token |
| Rotation | جایگزین کردن Token با Token جدید |
مثلاً:
Revocation:
"این Token را باطل کن."
Introspection:
"آیا این Token فعال است؟"
# معماری کلی
در یک سیستم OAuth 2.0 میتوان معماری زیر را داشت:
┌──────────────────────┐
│ Authorization Server │
│ │
│ Token Endpoint │
│ Revocation Endpoint │
│ Introspection │
└──────────┬───────────┘
│
│
Token Database
│
│
┌──────────┴───────────┐
│ │
▼ ▼
Client Resource Server
Authorization Server مسئول مدیریت Token است.
Resource Server مسئول ارائه API یا Resource است.
# Revocation Endpoint چیست؟
میتوانیم Endpointای مانند زیر داشته باشیم:
POST /oauth/revoke
Client میتواند Token موردنظر را برای Revocation ارسال کند.
مثلاً:
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=...
&token_type_hint=refresh_token
# token_type_hint چیست؟
Client میتواند نوع Token را نیز اعلام کند:
refresh_token
یا:
access_token
مثلاً:
token_type_hint=refresh_token
این مقدار میتواند به Authorization Server کمک کند تا Token را سریعتر پیدا کند.
اما Server نباید صرفاً به این مقدار اعتماد کند.
# پیادهسازی Revocation در PHP
ابتدا Token را دریافت میکنیم:
$token = $_POST['token'] ?? null;
if (!$token) {
http_response_code(400);
echo json_encode([
'error' => 'invalid_request'
]);
exit;
}
Hash Token را محاسبه میکنیم:
$tokenHash = hash(
'sha256',
$token
);
سپس Token را پیدا میکنیم:
$stmt = $pdo->prepare(
'SELECT *
FROM oauth_tokens
WHERE token_hash = :hash
LIMIT 1'
);
$stmt->execute([
'hash' => $tokenHash
]);
$record = $stmt->fetch(PDO::FETCH_ASSOC);
# اگر Token پیدا نشد چه کنیم؟
یکی از نکات مهم در Revocation Endpoint این است که نباید اطلاعات اضافی درباره وجود یا عدم وجود Token در اختیار Client قرار دهیم.
به جای پاسخهایی مانند:
{
"error": "token_not_found"
}
معمولاً Endpoint میتواند عملیات را بدون افشای اطلاعات داخلی انجام دهد.
مثلاً:
HTTP/1.1 200 OK
حتی اگر Token از قبل ناشناخته یا Revoke شده باشد.
این موضوع از افشای اطلاعات غیرضروری جلوگیری میکند.
# Revoke کردن Token
اگر Token پیدا شد:
$stmt = $pdo->prepare(
'UPDATE oauth_tokens
SET revoked_at = NOW()
WHERE id = :id
AND revoked_at IS NULL'
);
$stmt->execute([
'id' => $record['id']
]);
در نتیجه:
revoked_at = 2026-09-22 10:15:00
خواهد شد.
# Revocation فقط برای Refresh Token؟
خیر.
از نظر معماری ممکن است Access Token نیز Revoke شود.
اما نحوه مدیریت آن به نوع Access Token بستگی دارد.
اگر Access Token یک JWT Stateless باشد:
JWT
↓
Resource Server
↓
Local Signature Validation
Resource Server ممکن است اصلاً هر بار به Database مراجعه نکند.
در این شرایط Revocation فوری JWT دشوارتر است.
# مشکل Revocation در JWT
فرض کنید JWT زیر 15 دقیقه اعتبار دارد:
iat = 10:00
exp = 10:15
در ساعت:
10:05
آن را Revoke میکنیم.
اما اگر Resource Server فقط Signature و exp را بررسی کند، ممکن است هنوز آن را معتبر بداند.
چون:
10:05 < 10:15
است.
پس:
> Revoke کردن یک JWT به تنهایی کافی نیست؛ Resource Server باید سازوکار تشخیص Revocation نیز داشته باشد، اگر Revocation فوری لازم باشد.
# راههای مدیریت Revocation در JWT
چند روش معماری وجود دارد.
روش اول: کوتاه کردن عمر JWT
مثلاً:
Access Token = 5 تا 15 دقیقه
در این حالت حتی اگر Revoke فوری انجام نشود، عمر Token کوتاه است.
روش دوم: Blacklist
میتوان jti مربوط به Token را در یک Storage قرار داد:
Revoked JWT IDs
jti-001
jti-002
jti-003
Resource Server قبل از پذیرش Token بررسی میکند:
Is jti revoked?
این روش هزینه نگهداری و بررسی دارد.
روش سوم: Introspection
Resource Server برای بررسی وضعیت Token به Authorization Server مراجعه میکند.
API
│
│ Introspection
▼
Authorization Server
│
└── active = false
این روش کنترل مرکزی بیشتری ایجاد میکند.
# Introspection Endpoint
میتوانیم Endpoint زیر را داشته باشیم:
POST /oauth/introspect
درخواست:
POST /oauth/introspect
Content-Type: application/x-www-form-urlencoded
token=ACCESS_TOKEN
# چرا Introspection باید محافظت شود؟
Introspection Endpoint اطلاعات حساسی ارائه میکند.
اگر هر شخصی بتواند Token دلخواه را ارسال کند، ممکن است اطلاعات داخلی Tokenها افشا شود.
بنابراین Endpoint باید فقط برای Clientهای مجاز قابل استفاده باشد.
مثلاً:
Resource Server
│
│ Client Authentication
▼
Introspection Endpoint
# Authentication برای Introspection
بسته به نوع Client میتوان از روشهای مناسب Client Authentication استفاده کرد.
مثلاً:
Authorization: Basic base64(client_id:client_secret)
یا روشهای استاندارد دیگری که برای Client موردنظر تعریف شدهاند.
Client Secret نباید داخل Frontend یا JavaScript عمومی قرار گیرد.
# پیادهسازی ساده Introspection در PHP
ابتدا Token را دریافت میکنیم:
$token = $_POST['token'] ?? null;
if (!$token) {
http_response_code(400);
echo json_encode([
'error' => 'invalid_request'
]);
exit;
}
سپس Hash را تولید میکنیم:
$hash = hash(
'sha256',
$token
);
و Token را پیدا میکنیم:
$stmt = $pdo->prepare(
'SELECT *
FROM oauth_tokens
WHERE token_hash = :hash
LIMIT 1'
);
$stmt->execute([
'hash' => $hash
]);
$record = $stmt->fetch(PDO::FETCH_ASSOC);
# بررسی وضعیت Token
فرض کنیم Token وجود دارد.
ابتدا بررسی میکنیم Revoke نشده باشد:
$isRevoked = $record['revoked_at'] !== null;
سپس Expiration را بررسی میکنیم:
$isExpired =
strtotime($record['expires_at']) <= time();
در نهایت:
$active = !$isRevoked && !$isExpired;
و پاسخ:
echo json_encode([
'active' => $active
]);
# پاسخ کاملتر Introspection
اگر Token فعال باشد، میتوان اطلاعات موردنیاز را برگرداند:
echo json_encode([
'active' => true,
'client_id' => $record['client_id'],
'sub' => $record['user_id'],
'scope' => $record['scope'],
'aud' => $record['audience'],
'iss' => $record['issuer'],
'exp' => strtotime($record['expires_at']),
'iat' => strtotime($record['created_at']),
]);
البته اطلاعات دقیق پاسخ باید با مدل داده و سیاست امنیتی سیستم هماهنگ شود.
# اگر Token غیرفعال باشد
اگر Token:
- پیدا نشود
- Expired باشد
- Revoked باشد
میتوان پاسخ داد:
{
"active": false
}
نباید جزئیات غیرضروری درباره علت غیرفعال بودن Token را در پاسخ عمومی افشا کنیم.
# Introspection برای JWT چگونه است؟
فرض کنید Access Token یک JWT باشد.
Resource Server میتواند ابتدا Signature را بررسی کند.
اما اگر نیاز به Revocation مرکزی داشته باشیم، میتواند Introspection را نیز انجام دهد.
JWT
│
▼
Signature Validation
│
▼
Introspection
│
▼
active = true/false
البته این کار هزینه Network Request را به معماری اضافه میکند.
# آیا برای هر Request باید Introspection انجام شود؟
لزوماً نه.
اگر برای هر درخواست API یک درخواست جداگانه به Authorization Server ارسال شود:
100 API Requests
↓
100 Introspection Requests
این موضوع میتواند فشار زیادی ایجاد کند.
راهکارهایی مانند Cache کوتاهمدت میتوانند در برخی معماریها مفید باشند.
# Cache کردن نتیجه Introspection
فرض کنید نتیجه:
{
"active": true
}
را برای مدت بسیار کوتاهی Cache کنیم.
مثلاً:
TTL = 30 seconds
در این حالت تعداد درخواستها کاهش مییابد.
اما باید بدانیم:
> Cache کردن وضعیت Token باعث ایجاد یک فاصله زمانی بین Revocation و تشخیص آن میشود.
بنابراین TTL باید بر اساس نیاز امنیتی سیستم انتخاب شود.
# Revocation و Cache
فرض کنید:
10:00 → Token Active
10:01 → Token Revoked
اما نتیجه Introspection تا:
10:01:30
Cache شده باشد.
ممکن است Resource Server تا پایان Cache همچنان وضعیت قبلی را استفاده کند.
پس:
Security Requirement
↓
Cache TTL
باید با هم هماهنگ باشند.
# مدیریت Refresh Token با Revocation
در معماری پیشنهادی، Refresh Token باید قابلیت Revocation داشته باشد.
مثلاً:
oidc_refresh_tokens
id
user_id
client_id
token_hash
family_id
expires_at
revoked_at
replaced_by
created_at
last_used_at
هنگام Logout:
UPDATE oidc_refresh_tokens
SET revoked_at = NOW()
WHERE user_id = ?
AND revoked_at IS NULL;
اما اگر کاربر فقط یک دستگاه را Logout کند، بهتر است فقط Family مربوط به همان Session Revoke شود.
# Revoke کردن یک Token Family
مثلاً:
UPDATE oidc_refresh_tokens
SET revoked_at = NOW()
WHERE family_id = ?
AND revoked_at IS NULL;
در نتیجه:
Family A
RT-001 ❌
RT-002 ❌
RT-003 ❌
تمام زنجیره مربوط به آن Session از کار میافتد.
# Reuse Detection و Revocation
این دو مفهوم ارتباط بسیار نزدیکی دارند.
فرض کنید:
RT-001
↓
Rotated
↓
RT-002
و RT-001 قبلاً Revoke شده است.
اگر دوباره:
RT-001
ارسال شود:
Reuse Detected
↓
Revoke Family
یعنی:
RT-001 ❌
RT-002 ❌
این مکانیزم یکی از بخشهای مهم امنیت Refresh Token Rotation است.
# Revocation هنگام تغییر رمز عبور
در یک سیستم حساس میتوان بعد از تغییر رمز عبور:
Password Change
↓
Find Active Sessions
↓
Revoke Refresh Tokens
↓
Require Re-Authentication
انجام داد.
البته اینکه همه Sessionها باطل شوند یا فقط Sessionهای خاص، یک تصمیم معماری و سیاست امنیتی است.
# Revocation هنگام حذف دسترسی
فرض کنید کاربر دیگر نباید به یک Client خاص دسترسی داشته باشد.
مثلاً:
User
↓
Remove Access to Shop
میتوان Tokenهای مربوط به:
client_id = shop
را Revoke کرد.
این قابلیت در معماری چنداپلیکیشنی بسیار کاربردی است.
# Revocation در معماری چند اپلیکیشنی
فرض کنید:
Identity Provider
│
├── Portal
├── Shop
├── Admin
└── Mobile App
کاربر میتواند Sessionهای مختلفی داشته باشد.
Database میتواند چیزی شبیه این داشته باشد:
User: 1001
Portal → Family A
Shop → Family B
Mobile → Family C
در این حالت میتوانیم:
Logout Shop
را بدون Logout کردن:
Portal
Mobile
انجام دهیم.
# Audit Log برای Revocation
برای رویدادهای امنیتی بهتر است Audit Log داشته باشیم.
مثلاً:
token_revoked
refresh_token_reuse_detected
session_revoked
user_sessions_revoked
client_access_revoked
اما:
خود Token را داخل Audit Log ذخیره نکنید.
مثلاً:
Log::info('Refresh token revoked', [
'user_id' => $userId,
'client_id' => $clientId,
'family_id' => $familyId,
]);
# ساختار پیشنهادی پروژه PHP
میتوان منطق را از Controller جدا کرد:
app/
├── Controllers/
│ ├── TokenController.php
│ ├── RevocationController.php
│ └── IntrospectionController.php
│
├── Services/
│ ├── TokenService.php
│ ├── TokenRevocationService.php
│ └── TokenIntrospectionService.php
│
├── Repositories/
│ └── TokenRepository.php
│
└── Security/
└── ClientAuthenticator.php
مثلاً:
class TokenRevocationService
{
public function revoke(string $token): void
{
// Find token
// Mark as revoked
// Write audit event
}
}
و:
class TokenIntrospectionService
{
public function inspect(string $token): array
{
// Find token
// Check expiration
// Check revocation
// Return status
}
}
# Revocation با Logout چه تفاوتی دارد؟
Logout یک فرآیند کاربری است.
Revocation یک عملیات مربوط به اعتبار Credential است.
مثلاً:
Logout
├── Destroy Local Session
├── Revoke Tokens
└── OIDC Provider Logout
بنابراین Revocation میتواند بخشی از Logout باشد، اما با Logout یکی نیست.
# Revocation با Expiration چه تفاوتی دارد؟
Expiration خودکار است:
expires_at <= now
Revocation دستی یا سیستمی است:
revoked_at != null
بنابراین برای بررسی Token میتوانیم مفهوم کلی زیر را داشته باشیم:
$active =
!$expired &&
!$revoked;
البته در سیستم واقعی شرایط دیگری نیز ممکن است وجود داشته باشد.
# امنیت Endpointهای Revocation و Introspection
این Endpointها باید مانند سایر بخشهای Authentication با دقت محافظت شوند.
موارد مهم:
- HTTPS
- Client Authentication مناسب
- جلوگیری از Token Leakage
- Rate Limiting
- عدم Log کردن Token
- اعتبارسنجی ورودی
- کنترل دسترسی
- جلوگیری از اطلاعات اضافی در Error Response
- Audit Logging
- مدیریت مناسب Timeout
- Monitoring
# Rate Limiting
Introspection Endpoint ممکن است توسط Resource Server زیاد فراخوانی شود.
اگر Endpoint عمومی یا بدپیکربندی شود، ممکن است هدف سوءاستفاده قرار بگیرد.
بنابراین Rate Limiting و Authentication مناسب برای آن اهمیت دارد.
مثلاً میتوان Rate Limit جداگانه برای:
/token
/revoke
/introspect
در نظر گرفت.
# یک معماری کاملتر
در نهایت میتوان معماری را به شکل زیر تصور کرد:
┌──────────────────────┐
│ Identity Provider │
│ │
│ Authorization Server │
└──────────┬───────────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
/oauth/token /oauth/revoke /oauth/introspect
│ │ │
└────────────────┼────────────────┘
│
▼
Token Storage
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Refresh Token Access Token Token Family
و Resource Server:
Client
│
│ Access Token
▼
Resource Server
│
├── Local JWT Validation
│
└── Introspection
│
▼
Authorization Server
# چه زمانی JWT بهتر است؟
اگر نیاز دارید Resource Server بدون وابستگی دائمی به Authorization Server Token را بررسی کند، JWT میتواند گزینه مناسبی باشد.
مثلاً:
API
↓
Verify JWT Signature
↓
Check exp
↓
Check iss
↓
Check aud
↓
Check scope
اما Revocation فوری پیچیدهتر میشود.
# چه زمانی Introspection مفیدتر است؟
اگر نیاز دارید Authorization Server وضعیت Token را به صورت مرکزی کنترل کند:
API
↓
Introspection
↓
Central Token Status
میتواند معماری مناسبی باشد.
این موضوع بهخصوص برای Opaque Tokenها کاربرد زیادی دارد.
# آیا میتوان JWT و Introspection را با هم داشت؟
بله.
مثلاً:
Access Token = JWT
و Resource Server ابتدا Signature را بررسی کند.
برای APIهای حساس نیز:
JWT Validation
+
Introspection
انجام شود.
البته این معماری هزینه بیشتری دارد و باید بر اساس نیاز واقعی سیستم استفاده شود.
# چکلیست Token Revocation و Introspection
قبل از Production این موارد را بررسی کنید:
- [ ] Revocation Endpoint دارید.
- [ ] Introspection Endpoint در صورت نیاز طراحی شده است.
- [ ] Endpointهای حساس Authentication دارند.
- [ ] HTTPS فعال است.
- [ ] Access Token و Refresh Token به شکل امن مدیریت میشوند.
- [ ] Token خام در Log ذخیره نمیشود.
- [ ] Refresh Token قابل Revocation است.
- [ ] Token Family قابل Revoke است.
- [ ] Reuse Detection پیادهسازی شده است.
- [ ] JWT Revocation Strategy مشخص است.
- [ ] Expiration و Revocation از یکدیگر تفکیک شدهاند.
- [ ] Introspection Response اطلاعات اضافی افشا نمیکند.
- [ ] Rate Limiting وجود دارد.
- [ ] Audit Log برای رویدادهای مهم وجود دارد.
- [ ] Cache در صورت استفاده TTL مشخصی دارد.
- [ ] Race Condition در Token Management کنترل شده است.
- [ ] Logout با Token Management هماهنگ است.
# پرسشهای متداول
Token Revocation چیست؟
فرآیند باطل کردن Token قبل از پایان زمان طبیعی اعتبار آن است.
Token Introspection چیست؟
مکانیزمی است که Resource Server از Authorization Server درباره وضعیت یک Token سؤال میکند.
آیا Revocation همان Expiration است؟
خیر.
Expiration پایان طبیعی اعتبار Token است، اما Revocation باطل کردن عمدی Token قبل از Expiration است.
آیا JWT را میتوان Revoke کرد؟
بله، اما چون JWT میتواند Stateless باشد، Revocation فوری نیازمند مکانیزم اضافه مانند Blacklist، Introspection یا معماری مناسب دیگر است.
آیا باید Refresh Token را Revoke کنیم؟
بله، سیستم باید امکان Revoke کردن Refresh Token را داشته باشد؛ خصوصاً هنگام Logout، تغییرات امنیتی مهم، غیرفعال شدن حساب یا تشخیص سوءاستفاده.
آیا برای هر درخواست API باید Introspection انجام شود؟
الزاماً نه.
این موضوع به معماری، نوع Token، نیاز امنیتی و Performance سیستم بستگی دارد.
آیا نتیجه Introspection را میتوان Cache کرد؟
بله، اما Cache باعث میشود Revocation ممکن است با کمی تأخیر در Resource Server مشاهده شود. بنابراین TTL باید متناسب با نیاز امنیتی انتخاب شود.
# جمعبندی
در یک سیستم OAuth 2.0 یا OpenID Connect حرفهای، مدیریت Token فقط به تولید Token محدود نمیشود.
باید بتوانیم چرخه کامل آن را مدیریت کنیم:
Issue
↓
Validate
↓
Use
↓
Refresh
↓
Rotate
↓
Revoke
↓
Expire
دو مفهوم مهم این مقاله را نیز به خاطر بسپارید:
Token Revocation
→ Token را باطل میکند.
Token Introspection
→ وضعیت Token را بررسی میکند.
برای Refresh Token، داشتن:
Rotation
+
Reuse Detection
+
Revocation
یک ساختار امنیتی بسیار مهم ایجاد میکند.
برای Access Tokenهای JWT نیز باید از ابتدا مشخص کنیم که آیا Expiration کوتاهمدت کافی است یا سیستم به Revocation تقریباً فوری نیاز دارد.
در نهایت انتخاب بین JWT، Opaque Token، Introspection، Cache و Revocation باید بر اساس معماری واقعی سیستم انجام شود، نه صرفاً بر اساس اینکه یک روش در پروژه دیگری استفاده شده است.





