مقدمه
در معماریهای مدرن احراز هویت، معمولاً یک کاربر میتواند همزمان در چند اپلیکیشن مختلف وارد حساب خود باشد.
برای مثال تصور کنید یک مجموعه دارای این سرویسها باشد:
auth.example.comبرای مدیریت هویت کاربرانshop.example.comبرای فروشگاهpanel.example.comبرای پنل مدیریتportal.example.comبرای پنل کاربریapi.example.comبرای API
در چنین معماریای ممکن است کاربر تنها یک بار در Identity Provider وارد شود، اما در چند اپلیکیشن Session مستقل داشته باشد.
حالا اگر کاربر از Identity Provider خارج شود، یک سؤال مهم ایجاد میشود:
چطور به تمام اپلیکیشنها اطلاع دهیم که Session مربوط به این کاربر نیز باید بسته شود؟
یکی از راهکارهای استاندارد OpenID Connect برای این مسئله، Back-Channel Logout است.
در این روش، Identity Provider بهجای اینکه منتظر فعالیت مرورگر کاربر بماند، مستقیماً از طریق ارتباط Server-to-Server به اپلیکیشنهای متصل اطلاع میدهد که باید Session کاربر را ببندند.
در این مقاله با نحوه عملکرد Back-Channel Logout، Logout Token، مفهوم sid و sub، طراحی Database، اعتبارسنجی Token و پیادهسازی Endpoint مربوط به Logout در PHP آشنا میشویم.
# Back-Channel Logout چیست؟
Back-Channel Logout یکی از روشهای Logout در OpenID Connect است که در آن Identity Provider یا OP مستقیماً به Relying Party یا RP اطلاع میدهد که Session کاربر باید خاتمه پیدا کند.
به زبان ساده:
User
│
│ Logout
▼
Identity Provider
│
├──────────► Application A
│
├──────────► Application B
│
└──────────► Application C
ارتباط بین Identity Provider و اپلیکیشنها از طریق Backend انجام میشود.
بنابراین مرورگر کاربر لازم نیست برای انجام Logout بین تمام اپلیکیشنها حرکت کند.
# OP و RP در OpenID Connect چه هستند؟
قبل از ادامه باید دو اصطلاح مهم را بشناسیم.
OP چیست؟
OP مخفف:
OpenID Provider
است.
OP همان Identity Provider مرکزی است که وظیفه احراز هویت کاربر را بر عهده دارد.
مثلاً:
auth.example.com
میتواند OP سیستم ما باشد.
RP چیست؟
RP مخفف:
Relying Party
است.
هر اپلیکیشنی که از Identity Provider برای احراز هویت استفاده میکند، میتواند یک RP باشد.
مثلاً:
shop.example.com
panel.example.com
portal.example.com
هرکدام میتوانند یک RP باشند.
# چرا Back-Channel Logout اهمیت دارد؟
فرض کنیم کاربر در سه اپلیکیشن وارد شده است:
shop.example.com
panel.example.com
portal.example.com
و Sessionهای محلی آنها به شکل زیر هستند:
Shop Session
Panel Session
Portal Session
اگر کاربر از Identity Provider خارج شود، بستن فقط Session مرکزی کافی نیست.
ممکن است Sessionهای محلی همچنان فعال باشند.
در نتیجه معماری کامل باید بتواند Logout را به RPها منتقل کند.
Back-Channel Logout برای همین سناریو طراحی شده است.
# تفاوت Back-Channel Logout با روشهای دیگر Logout
در OpenID Connect چند مفهوم مختلف مربوط به Logout وجود دارد که نباید آنها را با یکدیگر اشتباه گرفت.
RP-Initiated Logout
در این روش، خود اپلیکیشن Logout را شروع میکند.
مثلاً:
User
↓
shop.example.com/logout
↓
Identity Provider
↓
Logout
این همان موضوعی است که در مقاله قبلی درباره آن صحبت کردیم.
OIDC Session Management
در Session Management، RP وضعیت Session کاربر در OP را بررسی میکند و میتواند تغییر وضعیت Session را تشخیص دهد.
این روش بیشتر بر تعامل Browser و OP متکی است.
Front-Channel Logout
در Front-Channel Logout، OP از طریق Browser کاربر به RPها اطلاعرسانی میکند.
بنابراین مرورگر نقش مهمی در فرآیند دارد.
Back-Channel Logout
در Back-Channel Logout، OP مستقیماً با Backend مربوط به RP ارتباط برقرار میکند:
OP
│
│ HTTPS POST
▼
RP Logout Endpoint
بنابراین این روش Server-to-Server است.
# معماری Back-Channel Logout
فرض کنیم معماری سیستم ما به این شکل باشد:
┌─────────────────┐
│ Identity │
│ Provider │
│ auth.example.com│
└────────┬────────┘
│
Logout Token │
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Shop │ │ Panel │ │ Portal │
│ RP │ │ RP │ │ RP │
└─────────┘ └─────────┘ └─────────┘
وقتی کاربر Logout میکند:
1. User → OP
2. OP Session را میبندد
3. OP Logout Token ایجاد میکند
4. OP Token را به RP ارسال میکند
5. RP Token را اعتبارسنجی میکند
6. RP Session محلی را میبندد
# Logout Token چیست؟
در Back-Channel Logout، Identity Provider یک JWT مخصوص Logout ایجاد میکند.
این Token با ID Token یکی نیست.
این نکته بسیار مهم است.
Logout Token معمولاً شامل Claimهایی برای شناسایی صادرکننده، مقصد و Session موردنظر است.
برای مثال:
{
"iss": "https://auth.example.com",
"aud": "shop-client",
"iat": 1760000000,
"jti": "unique-event-id",
"sid": "session-123",
"events": {
"http://schemas.openid.net/event/backchannel-logout": {}
}
}
# Claimهای مهم Logout Token
iss
مشخص میکند Token توسط چه Identity Providerای صادر شده است.
مثلاً:
"iss": "https://auth.example.com"
RP نباید صرفاً به محتوای Token اعتماد کند.
باید امضای Token و مقدار iss را نیز بررسی کند.
aud
مشخص میکند Logout Token برای کدام Client صادر شده است.
مثلاً:
"aud": "shop-client"
این Claim اهمیت زیادی دارد.
چون نباید یک RP بتواند Logout Token مربوط به RP دیگری را پردازش کند.
iat
زمان ایجاد Token را مشخص میکند.
"iat": 1760000000
این مقدار میتواند برای بررسی قدیمی یا غیرعادی بودن Token استفاده شود.
jti
شناسه یکتای Token است.
"jti": "logout-event-abc123"
وجود jti امکان ایجاد Replay Protection را فراهم میکند.
sid
شناسه Session در Identity Provider است.
مثلاً:
"sid": "op-session-123"
با استفاده از sid میتوان دقیقاً مشخص کرد کدام Session باید Logout شود.
sub
شناسه Subject یا کاربر است.
مثلاً:
"sub": "user-9821"
در بعضی سناریوها از sub برای شناسایی Sessionهای مرتبط با کاربر استفاده میشود.
events
این Claim مشخص میکند Token یک Logout Token است.
مثلاً:
"events": {
"http://schemas.openid.net/event/backchannel-logout": {}
}
بنابراین RP باید وجود Event مربوط به Back-Channel Logout را بررسی کند.
# تفاوت sid و sub
این دو Claim بسیار مهم هستند.
فرض کنید کاربر با شناسه:
user-100
سه Session داشته باشد:
Laptop
Mobile
Tablet
ممکن است هر Session یک sid متفاوت داشته باشد:
sid-laptop
sid-mobile
sid-tablet
در این حالت:
sub = user-100
کاربر را مشخص میکند.
اما:
sid = sid-laptop
یک Session خاص را مشخص میکند.
بنابراین sid برای Logout دقیق یک Session بسیار مفید است.
# طراحی Database برای Sessionهای OIDC
برای پیادهسازی درست Back-Channel Logout، بهتر است رابطه بین Session محلی، کاربر و Session مربوط به OP را ذخیره کنیم.
برای مثال:
CREATE TABLE oidc_sessions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
client_id VARCHAR(255) NOT NULL,
op_sid VARCHAR(255) NULL,
local_session_id_hash CHAR(64) NOT NULL,
created_at TIMESTAMP NULL,
last_seen_at TIMESTAMP NULL,
revoked_at TIMESTAMP NULL
);
در این جدول:
user_id
کاربر محلی سیستم است.
client_id
Client مربوط به RP است.
op_sid
شناسه Session در Identity Provider است.
local_session_id_hash
شناسه Session محلی را به صورت Hash شده نگه میدارد.
revoked_at
مشخص میکند Session بسته شده است یا خیر.
# چرا Session ID را بهتر است Hash کنیم؟
Session ID یک مقدار حساس است.
بنابراین بهتر است مقدار خام آن را در Database نگه نداریم.
برای مثال:
$hash = hash(
'sha256',
$sessionId
);
سپس مقدار Hash شده ذخیره شود.
در زمان جستجو نیز:
$hash = hash(
'sha256',
$sessionId
);
و جستجو بر اساس Hash انجام شود.
# ثبت Session هنگام Login
وقتی کاربر از طریق OIDC وارد اپلیکیشن میشود، اطلاعات Session را ثبت میکنیم.
مثلاً:
$sessionId = session_id();
$sessionHash = hash(
'sha256',
$sessionId
);
$db->insert('oidc_sessions', [
'user_id' => $userId,
'client_id' => $clientId,
'op_sid' => $opSid,
'local_session_id_hash' => $sessionHash,
'created_at' => date('Y-m-d H:i:s'),
]);
حالا سیستم میداند:
User
↓
Local Session
↓
OIDC Client
↓
OP Session
# Endpoint مربوط به Back-Channel Logout
هر RP باید یک Endpoint مشخص برای دریافت Logout Token داشته باشد.
مثلاً:
https://shop.example.com/oidc/backchannel-logout
Identity Provider این URL را از قبل میشناسد.
مثلاً هنگام ثبت Client:
{
"client_id": "shop-client",
"backchannel_logout_uri":
"https://shop.example.com/oidc/backchannel-logout"
}
# چرا Endpoint باید POST باشد؟
Logout Token باید از طریق HTTP POST ارسال شود.
برای مثال:
POST /oidc/backchannel-logout HTTP/1.1
Content-Type: application/x-www-form-urlencoded
و Body:
logout_token=eyJhbGciOiJSUzI1Ni...
نباید Endpoint را طوری طراحی کنیم که Logout Token از طریق GET دریافت شود.
# دریافت Logout Token در PHP
یک Endpoint ساده PHP میتواند به شکل زیر باشد:
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
$logoutToken = $_POST['logout_token'] ?? null;
if (!$logoutToken) {
http_response_code(400);
exit;
}
در این مرحله فقط Token را دریافت کردهایم.
هنوز نباید هیچ Sessionای را Logout کنیم.
# چرا نباید به JWT Decode شده اعتماد کنیم؟
یکی از اشتباهات خطرناک این است که Token را Decode کنیم و مستقیماً Claimها را بخوانیم.
مثلاً:
$payload = json_decode(
base64_decode($parts[1]),
true
);
و بعد بگوییم:
if ($payload['sid']) {
logoutUser($payload['sid']);
}
این کار امن نیست.
چون هر شخصی میتواند یک JWT جعلی بسازد.
ابتدا باید:
- Signature بررسی شود.
- Issuer بررسی شود.
- Audience بررسی شود.
- Event بررسی شود.
- زمان Token بررسی شود.
jtiبررسی شود.- سپس Session پیدا و بسته شود.
# اعتبارسنجی Signature
فرض کنید Identity Provider با RSA Token را امضا میکند.
RP باید کلید عمومی OP را از JWKS دریافت کند.
معمولاً:
https://auth.example.com/.well-known/jwks.json
کلید مناسب بر اساس kid پیدا میشود.
سپس Signature بررسی میشود.
در PHP میتوان از کتابخانههای معتبر JWT استفاده کرد.
به عنوان نمونه مفهومی:
$decoded = JWT::decode(
$logoutToken,
$publicKey
);
در پروژه واقعی باید مدیریت kid، JWKS و Key Rotation نیز به شکل صحیح انجام شود.
# بررسی Issuer
بعد از اعتبارسنجی Signature:
if ($claims->iss !== 'https://auth.example.com') {
throw new RuntimeException(
'Invalid issuer'
);
}
مقدار iss نباید از اطلاعات ارسالی کاربر یا URL درخواست گرفته شود.
باید از Configuration امن برنامه مشخص شود.
# بررسی Audience
مثلاً:
if ($claims->aud !== 'shop-client') {
throw new RuntimeException(
'Invalid audience'
);
}
در صورتی که Token برای Client دیگری باشد، نباید پردازش شود.
# بررسی events
باید مطمئن شویم Token واقعاً برای Back-Channel Logout صادر شده است.
مفهوم آن:
$logoutEvent =
'http://schemas.openid.net/event/backchannel-logout';
و سپس بررسی میکنیم که Event در Token وجود داشته باشد.
# بررسی sid و sub
Logout Token باید اطلاعات لازم برای شناسایی Session را داشته باشد.
مثلاً:
$sid = $claims->sid ?? null;
$sub = $claims->sub ?? null;
if (!$sid && !$sub) {
throw new RuntimeException(
'Missing sid or sub'
);
}
در صورتی که sid وجود داشته باشد، میتوان Session مشخصی را هدف قرار داد.
# پیدا کردن Session با sid
فرض کنیم Token شامل:
sid = op-session-123
باشد.
در Database:
$session = $db->fetchOne(
'SELECT *
FROM oidc_sessions
WHERE client_id = ?
AND op_sid = ?
AND revoked_at IS NULL',
[
$clientId,
$sid
]
);
اگر Session پیدا شد، آن را منقضی میکنیم.
# Logout کردن Session محلی
یکی از روشهای مناسب این است که Session را به جای حذف کامل، Revoked کنیم.
مثلاً:
$db->execute(
'UPDATE oidc_sessions
SET revoked_at = NOW()
WHERE id = ?',
[
$session['id']
]
);
در سیستم Session نیز باید Session واقعی کاربر باطل شود.
جزئیات این قسمت به Session Storage پروژه بستگی دارد.
# اگر Session از قبل Logout شده باشد چه؟
فرض کنید OP دوباره همان Logout Token را ارسال کند.
اگر Endpoint ما در این حالت خطا بدهد، طراحی خوبی نداریم.
Logout باید تا حد امکان Idempotent باشد.
یعنی:
Logout
Logout دوباره
Logout دوباره
نباید باعث ایجاد خطاهای زنجیرهای شود.
اگر Session قبلاً بسته شده باشد، میتوانیم پاسخ موفق بدهیم.
مثلاً:
HTTP/1.1 200 OK
یا:
HTTP/1.1 204 No Content
# جلوگیری از Replay Attack با jti
فرض کنید Logout Token دارای:
"jti": "logout-abc123"
باشد.
اگر این Token دوباره ارسال شود، RP باید بتواند تشخیص دهد که قبلاً پردازش شده است.
برای این کار میتوان جدول زیر داشت:
CREATE TABLE oidc_logout_events (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
jti VARCHAR(255) NOT NULL,
client_id VARCHAR(255) NOT NULL,
processed_at TIMESTAMP NOT NULL,
UNIQUE KEY unique_logout_event (jti, client_id)
);
بعد از پردازش موفق:
$db->insert('oidc_logout_events', [
'jti' => $jti,
'client_id' => $clientId,
'processed_at' => date('Y-m-d H:i:s'),
]);
به این ترتیب Token تکراری قابل شناسایی است.
# Logout کردن تمام Sessionهای کاربر
اگر معماری پروژه بر اساس sub کار کند، میتوان Sessionهای مرتبط با کاربر را پیدا کرد.
مثلاً:
$sessions = $db->fetchAll(
'SELECT *
FROM oidc_sessions
WHERE user_id = ?
AND client_id = ?
AND revoked_at IS NULL',
[
$userId,
$clientId
]
);
سپس Sessionها را Revoked میکنیم.
البته باید توجه داشت که sub و sid از نظر دامنه و هدف یکسان نیستند.
اگر هدف Logout یک Session خاص است، sid اطلاعات دقیقتری ارائه میدهد.
# چرا نباید بر اساس Email Logout کنیم؟
فرض کنید Logout Token دارای:
email = user@example.com
باشد.
این مقدار نباید کلید اصلی Logout باشد.
در OIDC، شناسایی هویت خارجی باید بر اساس شناسههای استاندارد Identity Provider انجام شود، نه Email.
برای Session نیز sid و در سناریوهای مربوطه sub بسیار مناسبتر هستند.
# ارسال Logout Token از Identity Provider
سمت OP باید برای هر RP یک Request ایجاد کند.
مثلاً:
$ch = curl_init(
$backchannelLogoutUri
);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'logout_token' => $logoutToken,
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$response = curl_exec($ch);
curl_close($ch);
در پروژه Production باید خطاهای شبکه، Timeout، Retry و Logging مناسب نیز مدیریت شوند.
# چرا نباید Logout تمام RPها را داخل Request کاربر انجام دهیم؟
فرض کنید 50 اپلیکیشن به Identity Provider متصل هستند.
اگر کاربر روی Logout کلیک کند و OP بخواهد داخل همان HTTP Request به 50 اپلیکیشن Request ارسال کند، احتمالاً با مشکلاتی مانند:
- Timeout
- کند شدن Logout
- شکست یک RP
- Retryهای نامناسب
- فشار روی OP
مواجه میشویم.
راهکار بهتر استفاده از Queue است.
# معماری پیشنهادی Production
میتوانیم معماری را به این شکل طراحی کنیم:
User
│
▼
Identity Provider
│
▼
Create Logout Event
│
▼
Queue
│
├────────► RP 1
│
├────────► RP 2
│
├────────► RP 3
│
└────────► RP 4
در این حالت Logout اصلی کاربر سریعتر انجام میشود و Workerها مسئول ارسال Notification هستند.
# Retry کردن Logout
ممکن است RP موقتاً در دسترس نباشد.
مثلاً:
Attempt 1 → Timeout
Attempt 2 → Timeout
Attempt 3 → Success
بنابراین Worker میتواند Retry داشته باشد.
اما Retry باید محدود باشد.
مثلاً:
1 دقیقه
5 دقیقه
15 دقیقه
و پس از تعداد مشخصی تلاش، Event به Dead Letter Queue منتقل شود.
# Timeout مناسب
نباید OP برای همیشه منتظر RP بماند.
مثلاً:
CURLOPT_TIMEOUT => 5
میتواند یک Timeout اولیه باشد.
مقدار دقیق باید با شرایط سیستم تعیین شود.
# امنیت Endpoint
Endpoint مربوط به Back-Channel Logout یکی از نقاط حساس سیستم است.
چند اصل مهم:
فقط HTTPS
https://example.com/oidc/backchannel-logout
نه:
http://example.com/...
فقط POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
اعتبارسنجی Signature
هرگز فقط Payload را Decode نکنید.
بررسی Issuer
iss
باید دقیقاً با OP مورد اعتماد مطابقت داشته باشد.
بررسی Audience
aud
باید Client فعلی را هدف گرفته باشد.
بررسی Event
Token باید Logout Event معتبر داشته باشد.
Replay Protection
jti را برای تشخیص Token تکراری مدیریت کنید.
Token را Log نکنید
این کار مناسب نیست:
error_log($logoutToken);
چون Token یک داده حساس است.
بهجای آن میتوان شناسهای مانند jti را با احتیاط و بدون اطلاعات حساس ثبت کرد.
# آیا CSRF برای Back-Channel Logout لازم است؟
این Endpoint با یک فرم HTML که کاربر در Browser ارسال کرده متفاوت است.
Back-Channel Logout یک ارتباط Server-to-Server است.
بنابراین CSRF Token معمول مرورگر معمولاً مکانیزم اصلی حفاظت این Endpoint نیست.
امنیت اصلی باید بر اساس مواردی مانند:
- TLS
- JWT Signature
- Issuer Validation
- Audience Validation
- Event Validation
- Replay Protection
- Endpoint Validation
باشد.
با این حال نباید صرفاً به «Server-to-Server بودن» اعتماد کرد.
هر Request همچنان باید بهدرستی اعتبارسنجی شود.
# مدیریت چند Session برای یک کاربر
یکی از مزایای استفاده از sid این است که میتوان Sessionهای مختلف را از هم تفکیک کرد.
مثلاً:
User 25
Laptop
sid = A100
Mobile
sid = B200
Tablet
sid = C300
اگر Logout Token دارای:
sid = B200
باشد، میتوان Session موبایل را هدف قرار داد.
این موضوع در سیستمهایی که کاربر همزمان روی چند دستگاه فعال است اهمیت زیادی دارد.
# ساختار پیشنهادی پروژه PHP
برای پروژههای بزرگ بهتر است تمام منطق را داخل Controller قرار ندهیم.
مثلاً:
app/
├── Controllers/
│ └── OidcBackChannelLogoutController.php
│
├── Services/
│ ├── OidcLogoutTokenValidator.php
│ ├── OidcSessionManager.php
│ └── BackChannelLogoutService.php
│
├── Repositories/
│ └── OidcSessionRepository.php
│
└── Jobs/
└── ProcessBackChannelLogout.php
در این ساختار:
Controller
Request را دریافت میکند.
Token Validator
Logout Token را اعتبارسنجی میکند.
Session Manager
Sessionهای محلی را مدیریت میکند.
Repository
تعامل با Database را انجام میدهد.
Job
کارهای غیرهمزمان را انجام میدهد.
# نمونه ساختار Service
مثلاً:
class BackChannelLogoutService
{
public function logout(
LogoutToken $token
): void {
$this->validateToken($token);
if ($token->sid) {
$this->logoutBySid(
$token->sid
);
return;
}
if ($token->sub) {
$this->logoutBySubject(
$token->sub
);
}
}
}
این ساختار باعث میشود منطق Logout قابل تست و توسعه باشد.
# ترتیب صحیح پردازش Logout Token
یک Flow منطقی میتواند این باشد:
Receive Request
↓
Check POST
↓
Read logout_token
↓
Validate JWT Signature
↓
Validate iss
↓
Validate aud
↓
Validate events
↓
Validate iat
↓
Check jti
↓
Find Session
↓
Revoke Session
↓
Record Event
↓
Return Success
این ترتیب باعث میشود قبل از اعتماد به اطلاعات Token، اعتبار آن بررسی شود.
# یک نمونه Endpoint کاملتر
ساختار مفهومی Endpoint:
public function logout(Request $request)
{
if (!$request->isMethod('post')) {
return response('', 405);
}
$logoutToken =
$request->input('logout_token');
if (!$logoutToken) {
return response('', 400);
}
try {
$claims =
$this->tokenValidator
->validate($logoutToken);
$this->replayGuard
->check($claims->jti);
$this->logoutService
->process($claims);
$this->replayGuard
->markProcessed($claims->jti);
return response('', 204);
} catch (
Throwable $e
) {
report($e);
return response('', 400);
}
}
در پروژه واقعی باید نوع Exceptionها، Logging، Metrics و سیاست پاسخدهی متناسب با معماری سیستم طراحی شوند.
# تفاوت Logout Token با ID Token
این دو را نباید یکی فرض کنیم.
| ویژگی | ID Token | Logout Token |
|---|---|---|
| هدف اصلی | احراز هویت کاربر | اطلاعرسانی Logout |
| استفاده در Login | بله | خیر |
| استفاده برای Logout | خیر | بله |
events مربوط به Logout | خیر | بله |
sid | ممکن است وجود داشته باشد | برای Session Logout مهم است |
jti | ممکن است استفاده شود | برای Replay Protection مهم است |
بنابراین نباید ID Token را بهعنوان Logout Token استفاده کنیم.
# اشتباهات رایج در Back-Channel Logout
۱. اعتماد به JWT بدون بررسی Signature
اشتباه:
decode($token);
logout();
راه صحیح:
Verify Signature
↓
Validate Claims
↓
Logout
۲. استفاده از Email برای شناسایی Session
Email شناسه مناسبی برای Session نیست.
از اطلاعات OIDC مناسب مانند sid و sub استفاده کنید.
۳. نداشتن Replay Protection
اگر jti را مدیریت نکنیم، یک Token قدیمی ممکن است چند بار پردازش شود.
۴. حذف کامل Session بدون ثبت وضعیت
در بسیاری از معماریها بهتر است وضعیت Revoked یا Logout شده نیز قابل ردیابی باشد.
۵. وابستگی به Browser
Back-Channel Logout برای Server-to-Server طراحی شده است.
نباید آن را مانند Front-Channel Logout پیادهسازی کنیم.
۶. اجرای تمام Requestها در یک HTTP Request
در معماریهای بزرگ بهتر است از Queue و Worker استفاده شود.
۷. نداشتن Timeout
یک RP خراب نباید Logout کل سیستم را برای همیشه متوقف کند.
۸. Log کردن Logout Token
Token را در فایل Log ذخیره نکنید.
۹. نادیده گرفتن Multi-Session
اگر کاربر روی چند دستگاه فعال است، طراحی Session باید این موضوع را در نظر بگیرد.
# Back-Channel Logout در Laravel
اگر پروژه شما Laravel باشد، میتوان همین معماری را بسیار تمیزتر پیادهسازی کرد.
مثلاً Route:
Route::post(
'/oidc/backchannel-logout',
[
OidcBackChannelLogoutController::class,
'handle'
]
);
Controller:
class OidcBackChannelLogoutController
{
public function handle(
Request $request
) {
$token = $request
->input('logout_token');
$claims = app(
LogoutTokenValidator::class
)->validate($token);
app(
BackChannelLogoutService::class
)->process($claims);
return response()->noContent();
}
}
و برای ارسال Logout به RPها میتوان از Queue استفاده کرد:
SendBackChannelLogout::dispatch(
$client,
$logoutToken
);
این مدل برای پروژههای Laravel بزرگ بسیار مناسبتر از قرار دادن تمام منطق در Controller است.
# معماری کامل پیشنهادی
برای یک سیستم واقعی میتوان ساختار زیر را در نظر گرفت:
┌─────────────────────┐
│ Identity Provider │
│ │
│ Auth + Session │
└──────────┬──────────┘
│
Logout Event
│
▼
┌──────────────┐
│ Queue │
└──────┬───────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
Worker A Worker B Worker C
│ │ │
▼ ▼ ▼
Shop Panel Portal
│ │ │
▼ ▼ ▼
Revoke Session Revoke Session Revoke Session
این معماری در مقیاس بالا قابل توسعهتر است.
# چه زمانی Back-Channel Logout انتخاب مناسبی است؟
Back-Channel Logout مخصوصاً زمانی مفید است که:
- چند RP داشته باشید.
- Identity Provider مرکزی داشته باشید.
- Logout یکپارچه اهمیت داشته باشد.
- Sessionهای محلی مستقل باشند.
- بخواهید Logout بدون وابستگی به Browser انجام شود.
- چند دستگاه یا Session همزمان داشته باشید.
- بخواهید مدیریت Logout در Backend انجام شود.
# چکلیست پیادهسازی Back-Channel Logout
قبل از Production این موارد را بررسی کنید:
- [ ] OP دارای Back-Channel Logout است.
- [ ] برای هر RP یک
backchannel_logout_uriثبت شده است. - [ ] Logout Token بهصورت JWT امضا میشود.
- [ ] Signature اعتبارسنجی میشود.
- [ ]
issبررسی میشود. - [ ]
audبررسی میشود. - [ ]
eventsبررسی میشود. - [ ]
sidوsubبه شکل صحیح مدیریت میشوند. - [ ]
jtiبرای Replay Protection استفاده میشود. - [ ] Sessionهای OIDC در Database قابل شناسایی هستند.
- [ ] Logout عملیات Idempotent دارد.
- [ ] Request دارای Timeout است.
- [ ] Retry مناسب وجود دارد.
- [ ] Token در Log ذخیره نمیشود.
- [ ] Endpoint فقط HTTPS است.
- [ ] Endpoint فقط POST را قبول میکند.
- [ ] برای تعداد زیاد RP از Queue استفاده شده است.
# سوالات متداول
آیا Back-Channel Logout همان Logout معمولی است؟
خیر.
Logout معمولی میتواند فقط Session محلی را ببندد، در حالی که Back-Channel Logout مکانیزمی برای اطلاعرسانی Logout از OP به RPها است.
آیا Back-Channel Logout به مرورگر کاربر نیاز دارد؟
خیر.
ارتباط اصلی بین OP و RP بهصورت Server-to-Server انجام میشود.
آیا Logout Token همان ID Token است؟
خیر.
Logout Token برای اطلاعرسانی Logout طراحی شده و Claimهای مخصوص خودش را دارد.
sid چه کاربردی دارد؟
sid شناسه یک Session در Identity Provider است و برای مشخص کردن Session موردنظر در Logout بسیار مهم است.
sub چه کاربردی دارد؟
sub شناسه Subject یا کاربر در محدوده Identity Provider است و در سناریوهایی که نیاز به شناسایی Sessionهای مرتبط با کاربر داریم میتواند استفاده شود.
آیا باید Logout Token را در Database ذخیره کنیم؟
ذخیره jti یا شناسه رویداد برای Replay Protection میتواند مفید باشد، اما ذخیره کامل Token معمولاً ضرورتی ندارد و میتواند ریسک امنیتی ایجاد کند.
اگر یکی از RPها Down باشد چه میشود؟
Identity Provider نباید برای همیشه منتظر آن RP بماند. استفاده از Timeout، Retry و Queue باعث میشود خرابی یک RP کل فرآیند Logout را مختل نکند.
# جمعبندی
Back-Channel Logout یکی از اجزای مهم معماری Logout در OpenID Connect برای سیستمهایی است که چند اپلیکیشن و یک Identity Provider مرکزی دارند.
در این معماری، Identity Provider بعد از Logout کاربر، یک Logout Token ایجاد کرده و آن را به Endpoint مخصوص هر RP ارسال میکند.
RP نیز باید قبل از Logout کردن Session:
Signature
Issuer
Audience
Event
Session
Replay
را بهدرستی بررسی کند.
مهمترین نکته این است که Back-Channel Logout فقط ارسال یک HTTP Request نیست؛ بلکه یک فرآیند امنیتی کامل است که باید در آن ارتباط بین User، OP، RP، Session و Logout Token به شکل صحیح مدیریت شود.
در معماریهای بزرگ نیز بهتر است ارسال Logout Notification به RPها با Queue و Worker انجام شود تا خرابی یا کندی یک سرویس، روی کل سیستم احراز هویت اثر نگذارد.
در ادامه این مسیر، شناخت تفاوت Back-Channel Logout با Front-Channel Logout اهمیت زیادی دارد و میتواند به طراحی یک معماری کامل Single Logout در OpenID Connect کمک کند.





