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

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

پیوندهای سریع
محصولات فیزیکیمحصولات دیجیتالدوره‌های آموزشی
سی ام اس نویس
مجله سی ام اس نویستماس با ماپنل کاربری
سی ام اس نویس — تمامی حقوق برای سی ام اس نویس محفوظ است. © 1406 تماس با ما09307711941
مدیریت Access Token و Refresh Token در OpenID Connect با PHP؛ ذخیره‌سازی، انقضا، Rotation و Revocation

مدیریت Access Token و Refresh Token در OpenID Connect با PHP؛ ذخیره‌سازی، انقضا، Rotation و Revocation

ممدیر کل سیستم ۳۱ شهریور ۱۴۰۵ ۲۱ دقیقه مطالعه ۱ بازدید
مدیریت Access Token و Refresh Token در OpenID Connect با PHP؛ ذخیره‌سازی، انقضا، Rotation و Revocation

# مدیریت 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 معمولاً ویژگی‌هایی مانند موارد زیر دارد:

  • iss
  • sub
  • aud
  • scope
  • exp
  • iat

اگر 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 دارند اهمیت بیشتری پیدا می‌کند.

نظرات (0)

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

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

مقالات مرتبط

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

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

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

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

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

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

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

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

۲۲ دقیقه