سی ام اس نویسسی ام اس نویسیاد بگیر، سیستم خودتو بنویس
مجلهتماس با ما
سی ام اس نویسسی ام اس نویس

CMS نویس مرجعی برای آموزش برنامه نویسی وب و PHP، توسعه افزونه‌های اختصاصی وردپرس، طراحی ورک‌فلو و اتوماسیون با n8n و توسعه CMS های اختصاصی است. هدف ما ارائه آموزش‌های کاربردی و راهکارهای حرفه‌ای برای توسعه‌دهندگان، کسب‌وکارها و علاقه‌مندان به دنیای برنامه‌نویسی است.

پیوندهای سریع
محصولات فیزیکیمحصولات دیجیتالدوره‌های آموزشی
سی ام اس نویس
مجله سی ام اس نویستماس با ماپنل کاربری
سی ام اس نویس — تمامی حقوق برای سی ام اس نویس محفوظ است. © 1406 تماس با ما09307711941
پیاده‌سازی Back-Channel Logout در OpenID Connect با PHP؛ مدیریت خروج یکپارچه از سمت Identity Provider

پیاده‌سازی Back-Channel Logout در OpenID Connect با PHP؛ مدیریت خروج یکپارچه از سمت Identity Provider

ممدیر کل سیستم ۳۱ شهریور ۱۴۰۵ ۲۳ دقیقه مطالعه ۲ بازدید
پیاده‌سازی Back-Channel Logout در OpenID Connect با PHP؛ مدیریت خروج یکپارچه از سمت Identity Provider

مقدمه

در معماری‌های مدرن احراز هویت، معمولاً یک کاربر می‌تواند هم‌زمان در چند اپلیکیشن مختلف وارد حساب خود باشد.

برای مثال تصور کنید یک مجموعه دارای این سرویس‌ها باشد:

  • 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 جعلی بسازد.

ابتدا باید:

  1. Signature بررسی شود.
  2. Issuer بررسی شود.
  3. Audience بررسی شود.
  4. Event بررسی شود.
  5. زمان Token بررسی شود.
  6. jti بررسی شود.
  7. سپس 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 TokenLogout 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 کمک کند.

نظرات (0)

نظرات پس از تأیید مدیر نمایش داده می‌شوند

هنوز نظری ثبت نشده است؛ اولین نفر باشید!

مقالات مرتبط

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

۲۴ دقیقه
ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

۲۲ دقیقه
ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

۲۲ دقیقه
مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

۲۲ دقیقه