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

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

پیوندهای سریع
محصولات فیزیکیمحصولات دیجیتالدوره‌های آموزشی
سی ام اس نویس
مجله سی ام اس نویستماس با ماپنل کاربری
سی ام اس نویس — تمامی حقوق برای سی ام اس نویس محفوظ است. © 1406 تماس با ما09307711941
مدیریت JWT Key Rotation و JWKS در PHP؛ ساخت و چرخش امن کلیدهای RSA

مدیریت JWT Key Rotation و JWKS در PHP؛ ساخت و چرخش امن کلیدهای RSA

ممدیر کل سیستم ۳۱ شهریور ۱۴۰۵ ۲۲ دقیقه مطالعه ۱ بازدید
مدیریت JWT Key Rotation و JWKS در PHP؛ ساخت و چرخش امن کلیدهای RSA

در سیستم‌هایی که از JWT، OAuth 2.0 و OpenID Connect استفاده می‌کنند، یکی از مهم‌ترین بخش‌های امنیتی، مدیریت کلیدهای رمزنگاری است.

فرض کنید یک OpenID Connect Provider داریم که ID Tokenهای خود را با RSA امضا می‌کند.

در ابتدا همه‌چیز ساده به نظر می‌رسد:


Private Key

    ↓

Sign JWT

    ↓

ID Token

و Client نیز Public Key را دریافت می‌کند:


JWKS

 ↓

Public Key

 ↓

Verify JWT

اما بعد از مدتی یک سؤال مهم مطرح می‌شود:

> اگر بخواهیم Private Key را تغییر دهیم، چه اتفاقی برای Tokenهای قبلی می‌افتد؟

اگر کلید جدید را جایگزین کلید قبلی کنیم و Public Key قدیمی را بلافاصله حذف کنیم، ممکن است Clientهایی که هنوز در حال اعتبارسنجی Tokenهای قبلی هستند با خطا مواجه شوند.

اینجاست که مفهوم Key Rotation اهمیت پیدا می‌کند.

در این مقاله می‌خواهیم به‌صورت عملی بررسی کنیم:

  • JWK چیست؟
  • JWKS چیست؟
  • RSA Key Pair چگونه کار می‌کند؟
  • kid چیست؟
  • چرا باید کلیدها را Rotate کنیم؟
  • چگونه چند کلید را همزمان منتشر کنیم؟
  • چگونه کلید قدیمی را بدون قطع Login حذف کنیم؟
  • JWKS Endpoint چگونه ساخته می‌شود؟
  • Client چگونه کلید مناسب را پیدا می‌کند؟
  • Key Rotation در OpenID Connect چگونه انجام می‌شود؟
  • چه اشتباهاتی می‌تواند باعث اختلال یا ضعف امنیتی شود؟

> نکته مهم: این مقاله درباره معماری و مدیریت کلید است. برای عملیات حساس JWT/JOSE در Production بهتر است از کتابخانه‌های استاندارد و تست‌شده استفاده شود و عملیات رمزنگاری به‌صورت دستی پیاده‌سازی نشود.


# JWT برای امضا به کلید نیاز دارد

JWT می‌تواند با روش‌های مختلفی امضا شود.

برای مثال:


HS256

RS256

ES256

EdDSA

در معماری OIDC معمولاً استفاده از الگوریتم‌های نامتقارن مزیت مهمی دارد.

برای مثال:


RS256

در این حالت دو کلید داریم:


Private Key

Public Key

Private Key برای امضا استفاده می‌شود:


Private Key

      ↓

Sign JWT

      ↓

JWT

Public Key برای Verify کردن Signature استفاده می‌شود:


JWT

 ↓

Public Key

 ↓

Verify


# تفاوت رمزنگاری متقارن و نامتقارن

در الگوریتم متقارن، یک Secret مشترک داریم:


Secret

  │

  ├── Sign

  │

  └── Verify

در الگوریتم نامتقارن:


Private Key

     │

     └── Sign



Public Key

     │

     └── Verify

این ویژگی برای OpenID Connect بسیار مهم است.

Provider می‌تواند Public Key را در اختیار همه Clientها قرار دهد، بدون اینکه Private Key را افشا کند.


# JWK چیست؟

JWK مخفف:


JSON Web Key

است.

JWK یک قالب استاندارد برای نمایش اطلاعات یک کلید رمزنگاری در قالب JSON است.

برای مثال یک RSA JWK می‌تواند چیزی شبیه این باشد:


{

    "kty": "RSA",

    "kid": "key-2026-01",

    "use": "sig",

    "alg": "RS256",

    "n": "...",

    "e": "AQAB"

}

هر فیلد وظیفه مشخصی دارد.


# kty چیست؟

kty نوع کلید را مشخص می‌کند.

برای RSA:


RSA

مثلاً:


{

    "kty": "RSA"

}


# kid چیست؟

kid مخفف:


Key ID

است.

فرض کنید Provider دو کلید دارد:


key-2026-01

key-2026-06

JWT هنگام امضا مشخص می‌کند از کدام کلید استفاده شده است:


{

    "alg": "RS256",

    "kid": "key-2026-06"

}

Client سپس از JWKS دنبال همان kid می‌گردد.


# چرا kid مهم است؟

فرض کنید JWKS ما دارای دو کلید باشد:


JWKS

│

├── key-2026-01

└── key-2026-06

و JWT Header بگوید:


kid = key-2026-06

Client می‌فهمد باید کلید:


key-2026-06

را برای Verify کردن Signature استفاده کند.

بنابراین kid پل ارتباطی بین JWT و JWK است.


# JWKS چیست؟

JWKS مخفف:


JSON Web Key Set

است.

اگر JWK یک کلید باشد:


JWK = یک Key

JWKS مجموعه‌ای از کلیدهاست:


JWKS

 ├── JWK 1

 ├── JWK 2

 └── JWK 3

یک JWKS Response معمولاً ساختاری شبیه این دارد:


{

    "keys": [

        {

            "kty": "RSA",

            "kid": "key-2026-01",

            "use": "sig",

            "alg": "RS256",

            "n": "...",

            "e": "AQAB"

        },

        {

            "kty": "RSA",

            "kid": "key-2026-06",

            "use": "sig",

            "alg": "RS256",

            "n": "...",

            "e": "AQAB"

        }

    ]

}


# JWKS Endpoint در OpenID Connect

در یک OIDC Provider معمولاً Endpointی مانند:


/.well-known/jwks.json

داریم.

مثلاً:


https://auth.example.com/.well-known/jwks.json

Client از Discovery می‌تواند URL این Endpoint را دریافت کند:


{

    "jwks_uri":

        "https://auth.example.com/.well-known/jwks.json"

}


# ساخت RSA Key Pair در PHP

برای ساخت RSA Key می‌توان از OpenSSL استفاده کرد.

نمونه:


<?php



$config = [

    'private_key_bits' => 4096,

    'private_key_type' => OPENSSL_KEYTYPE_RSA,

];



$key = openssl_pkey_new($config);



if ($key === false) {

    throw new RuntimeException(

        'Unable to generate RSA key'

    );

}



openssl_pkey_export(

    $key,

    $privateKey

);



$details = openssl_pkey_get_details($key);



$publicKey = $details['key'];

در این مثال یک RSA Key Pair ایجاد می‌شود.


# چرا 4096 بیت؟

RSA می‌تواند با اندازه‌های مختلف کلید استفاده شود.

مثلاً:


2048

3072

4096

انتخاب اندازه کلید باید با توجه به الگوریتم، سطح امنیت، کارایی و استانداردهای مورد استفاده پروژه انجام شود.

برای یک سیستم Production، انتخاب اندازه کلید نباید صرفاً بر اساس یک عدد ثابت انجام شود.


# ذخیره Private Key

Private Key باید با دقت زیادی محافظت شود.

مثلاً:


storage/keys/

    oidc-private.pem

اما این مسیر نباید Public باشد.

یعنی نباید:


public/keys/oidc-private.pem

باشد.

همچنین:


.git/

نباید شامل Private Key باشد.


# Permission فایل Private Key

روی Linux باید دسترسی فایل به شکل محدود تنظیم شود.

مثلاً:


chmod 600 storage/keys/oidc-private.pem

و Owner باید همان کاربری باشد که PHP Application با آن اجرا می‌شود.


# Private Key را داخل Git قرار ندهید

این کار بسیار خطرناک است:


git add oidc-private.pem

git commit

git push

حتی اگر بعداً فایل را حذف کنید، ممکن است کلید همچنان در تاریخچه Git باقی مانده باشد.

اگر Private Key به‌صورت عمومی افشا شد، باید آن را Compromised در نظر گرفت و Key Rotation انجام داد.


# بهتر است Private Key کجا باشد؟

بسته به معماری می‌توان از موارد مختلف استفاده کرد:


Environment Secret

Secret Manager

Vault

KMS

HSM

Encrypted Storage

در سیستم‌های حساس‌تر می‌توان مدیریت کلید را از خود Application نیز جدا کرد.


# ساخت Key ID

هر کلید باید یک kid داشته باشد.

مثلاً:


$kid = 'key-' . date('Y-m');

اما برای Production بهتر است شناسه کلید:

  • پایدار باشد
  • Unique باشد
  • با تغییر محتوا تغییر نکند
  • اطلاعات حساس در آن قرار نگیرد

مثلاً:


rsa-2026-09-a1


# ساختار ذخیره کلیدها

می‌توانیم اطلاعات هر Key را در دیتابیس نگهداری کنیم.

مثلاً:


CREATE TABLE oidc_signing_keys (

    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,



    kid VARCHAR(100) NOT NULL UNIQUE,



    algorithm VARCHAR(20) NOT NULL,



    private_key_path TEXT NOT NULL,



    public_key TEXT NOT NULL,



    status VARCHAR(30) NOT NULL,



    created_at DATETIME NOT NULL,



    activated_at DATETIME NULL,



    retired_at DATETIME NULL

);

Status می‌تواند مثلاً:


active

retiring

revoked

باشد.


# چرا کلید را در دیتابیس ذخیره نکنیم؟

می‌توان اطلاعات Public Key را در Database ذخیره کرد.

اما Private Key بهتر است در Secret Storage یا فایل محافظت‌شده قرار گیرد.

در معماری‌های حساس‌تر:


Database

   │

   └── Metadata



Secret Manager / KMS

   │

   └── Private Key

می‌تواند معماری مناسب‌تری باشد.


# Lifecycle کلید

یک کلید معمولاً چرخه‌ای شبیه این دارد:


Created

   ↓

Active

   ↓

Retiring

   ↓

Revoked / Removed

اما نکته مهم این است که Active بودن کلید با منتشر بودن Public Key یکی نیست.


# Active Key چیست؟

Active Key همان کلیدی است که Provider برای Tokenهای جدید از آن استفاده می‌کند.

مثلاً:


Current Signing Key:



kid = key-2026-09

Tokenهای جدید با این Key امضا می‌شوند.


# Retiring Key چیست؟

وقتی می‌خواهیم کلید جدیدی فعال کنیم:


Old Key

New Key

کلید قدیمی دیگر برای Tokenهای جدید استفاده نمی‌شود.

اما ممکن است هنوز Tokenهایی وجود داشته باشند که با کلید قدیمی امضا شده‌اند.

بنابراین Public Key قدیمی باید برای مدتی در JWKS باقی بماند.


# مهم‌ترین اصل Key Rotation

نباید این کار را انجام دهیم:


Old Key

   ↓

Delete Immediately



New Key

   ↓

Create

زیرا Client ممکن است هنوز Tokenی داشته باشد که با Old Key امضا شده است.

بهتر است:


Old Key

New Key

را همزمان منتشر کنیم.


# Key Rotation صحیح

فرآیند مناسب:


مرحله 1

Old Key Active



مرحله 2

New Key Created



مرحله 3

Old + New در JWKS



مرحله 4

New Key Active



مرحله 5

Tokenهای جدید با New Key



مرحله 6

صبر تا Tokenهای Old منقضی شوند



مرحله 7

Old Key از JWKS حذف شود

به شکل تصویری:


زمان

─────────────────────────────────────>



Old Key

██████████████████████



New Key

          ███████████████████████████


# مثال عملی Key Rotation

فرض کنیم:


Old:

kid = rsa-2026-08



New:

kid = rsa-2026-09

قبل از Rotation:


{

    "keys": [

        {

            "kid": "rsa-2026-08",

            "kty": "RSA",

            "alg": "RS256",

            "use": "sig"

        }

    ]

}

ابتدا کلید جدید را اضافه می‌کنیم:


{

    "keys": [

        {

            "kid": "rsa-2026-08",

            "kty": "RSA",

            "alg": "RS256",

            "use": "sig"

        },

        {

            "kid": "rsa-2026-09",

            "kty": "RSA",

            "alg": "RS256",

            "use": "sig"

        }

    ]

}

بعد:


Signing Key = rsa-2026-09

می‌شود.

اما Public Key قدیمی همچنان در JWKS باقی می‌ماند.


# چرا باید قبل از فعال کردن کلید جدید آن را در JWKS منتشر کنیم؟

فرض کنید Provider بلافاصله با کلید جدید Token صادر کند:


New Token

kid = rsa-2026-09

اما Client هنوز JWKS قدیمی را Cache کرده باشد:


Client JWKS

kid = rsa-2026-08

Client نمی‌تواند Signature را Verify کند.

به همین دلیل بهتر است:


Publish New Public Key

        ↓

Wait / Allow Cache Propagation

        ↓

Start Signing With New Key

انجام شود.


# JWKS Cache چیست؟

Clientها ممکن است JWKS را برای مدتی Cache کنند تا در هر Request مجبور نباشند دوباره Endpoint را فراخوانی کنند.

مثلاً:


Client

  │

  ▼

JWKS

  │

  ▼

Cache

بنابراین Provider باید Rotation را با در نظر گرفتن Cache انجام دهد.


# Cache-Control برای JWKS

می‌توان روی JWKS Headerهایی مانند:


Cache-Control: public, max-age=3600

تنظیم کرد.

مقدار مناسب به معماری و سرعت مورد نیاز برای Rotation بستگی دارد.

اگر max-age بسیار طولانی باشد، انتشار کلید جدید ممکن است دیرتر در Clientها دیده شود.


# آیا باید JWKS را بدون Cache کنیم؟

معمولاً نه.

JWKS یک Endpoint عمومی و نسبتاً پایدار است و Cache می‌تواند فشار روی Provider را کاهش دهد.

اما در طراحی Cache باید سناریوی:


Key Rotation

را نیز در نظر بگیریم.


# JWKS Endpoint در PHP

یک Endpoint ساده:


<?php



header('Content-Type: application/json');



header(

    'Cache-Control: public, max-age=3600'

);



$keys = [

    [

        'kty' => 'RSA',

        'kid' => 'rsa-2026-09',

        'use' => 'sig',

        'alg' => 'RS256',

        'n' => $modulus,

        'e' => $exponent,

    ]

];



echo json_encode(

    ['keys' => $keys],

    JSON_UNESCAPED_SLASHES

);

در پروژه واقعی باید فقط کلیدهای Public مربوط به وضعیت انتشار را برگردانیم.


# Private Key نباید در JWKS باشد

این اشتباه بسیار خطرناک است:


{

    "kty": "RSA",

    "kid": "rsa-2026-09",

    "d": "PRIVATE_EXPONENT"

}

پارامترهایی مانند d مربوط به بخش Private Key هستند و نباید در Public JWKS قرار بگیرند.

JWKS عمومی باید فقط اطلاعات مورد نیاز برای Verify کردن Signature را منتشر کند.


# پارامتر n چیست؟

در RSA، پارامتر:


n

Modulus است.

این مقدار یکی از بخش‌های Public Key است و در JWK معمولاً به شکل Base64URL نمایش داده می‌شود.


# پارامتر e چیست؟

پارامتر:


e

Exponent عمومی RSA است.

یک مقدار رایج:


AQAB

است که معمولاً نمایانگر مقدار استاندارد 65537 است.


# تبدیل RSA Public Key به JWK

برای ساخت JWK از Public Key باید پارامترهای RSA استخراج شوند.

مثلاً:


$details = openssl_pkey_get_details(

    $publicKey

);



$modulus = $details['rsa']['n'];

$exponent = $details['rsa']['e'];

اما این مقادیر باید با Encoding استاندارد JWK به Base64URL تبدیل شوند.

یک Helper آموزشی:


function base64UrlEncode(

    string $data

): string {



    return rtrim(

        strtr(

            base64_encode($data),

            '+/',

            '-_'

        ),

        '='

    );

}

سپس:


$n = base64UrlEncode(

    $details['rsa']['n']

);



$e = base64UrlEncode(

    $details['rsa']['e']

);

و:


$jwk = [

    'kty' => 'RSA',

    'kid' => 'rsa-2026-09',

    'use' => 'sig',

    'alg' => 'RS256',

    'n' => $n,

    'e' => $e,

];

> این نمونه برای درک ساختار JWK است. در Production، تبدیل کلیدها و اعتبارسنجی JOSE را به کتابخانه معتبر بسپارید.


# چند Key در JWKS

یک Provider می‌تواند همزمان چند Key منتشر کند:


{

    "keys": [

        {

            "kty": "RSA",

            "kid": "rsa-2026-08",

            "use": "sig",

            "alg": "RS256",

            "n": "...",

            "e": "AQAB"

        },

        {

            "kty": "RSA",

            "kid": "rsa-2026-09",

            "use": "sig",

            "alg": "RS256",

            "n": "...",

            "e": "AQAB"

        }

    ]

}

Client باید بر اساس:


kid

کلید صحیح را انتخاب کند.


# Client چگونه Key مناسب را پیدا می‌کند؟

JWT Header:


{

    "alg": "RS256",

    "kid": "rsa-2026-09"

}

JWKS:


rsa-2026-08

rsa-2026-09

Client:


kid = rsa-2026-09

        ↓

Search JWKS

        ↓

Find matching JWK

        ↓

Build Public Key

        ↓

Verify Signature


# اگر kid پیدا نشد چه کنیم؟

اگر Client یک JWT با:


kid = rsa-2026-10

دریافت کرد ولی JWKS آن را ندارد، نباید به‌صورت کورکورانه Token را قبول کنیم.

می‌توان:


1. JWKS Cache را Refresh کرد

2. دوباره kid را جستجو کرد

3. اگر نبود، Token را رد کرد

این رفتار در Clientهای OIDC اهمیت زیادی دارد.


# چرا Refresh کردن JWKS می‌تواند مفید باشد؟

فرض کنید Client JWKS را Cache کرده:


Old JWKS

Provider اخیراً Key جدیدی منتشر کرده:


New JWKS

Client Token جدید را دریافت می‌کند:


kid = new-key

اگر Key در Cache نباشد، Client می‌تواند یک بار JWKS را Refresh کند.

سپس:


New JWKS

   ↓

Find new-key

   ↓

Verify

اما باید مراقب بود این رفتار تبدیل به درخواست‌های بی‌نهایت به JWKS نشود.


# جلوگیری از JWKS Fetch Storm

فرض کنید تعداد زیادی Request همزمان با kid جدید وارد شوند.

اگر همه آنها JWKS را جداگانه درخواست کنند:


1000 Requests

      │

      ├── JWKS

      ├── JWKS

      ├── JWKS

      ├── JWKS

      └── ...

ممکن است فشار زیادی روی Provider ایجاد شود.

بهتر است Client یک مکانیزم Cache و Refresh کنترل‌شده داشته باشد.

مثلاً:


Unknown kid

     ↓

One JWKS Refresh

     ↓

Update Cache

     ↓

Retry Verification


# Key Rotation و Token Lifetime

مدت نگهداری کلید قدیمی باید با Token Lifetime هماهنگ باشد.

مثلاً:


Access/ID Token:

1 ساعت

اگر کلید قدیمی بلافاصله حذف شود، ممکن است Tokenهایی که هنوز معتبر هستند قابل Verify نباشند.

بنابراین Old Public Key باید حداقل تا زمانی که Tokenهای صادرشده با آن کلید دیگر معتبر نیستند در دسترس بماند؛ با در نظر گرفتن Cache و سیاست‌های عملیاتی سیستم.


# یک Timeline واقعی

فرض کنیم:


00:00

Old Key فعال



00:50

New Key در JWKS منتشر می‌شود



01:00

New Key فعال می‌شود



01:00+

Tokenهای جدید با New Key صادر می‌شوند



...

بعد از گذشت زمان کافی:


Old Tokenها منقضی شده‌اند

سپس:


Old Key

    ↓

Remove from JWKS


# Rotation نباید باعث Logout کاربران شود

اگر Key Rotation درست انجام شود:


Key Rotation

      ≠

User Logout

کاربر نباید صرفاً به خاطر تغییر کلید Signing مجبور به Login مجدد شود.

این یکی از مزایای طراحی صحیح JWKS است.


# Emergency Key Rotation

گاهی Rotation برنامه‌ریزی‌شده نیست.

مثلاً Private Key احتمالاً افشا شده است.

در این حالت باید سریع‌تر عمل کنیم:


Compromised Key

      ↓

Stop Signing

      ↓

Revoke Key

      ↓

Activate New Key

      ↓

Publish New JWKS

      ↓

Invalidate affected Tokens

در چنین سناریویی ممکن است لازم باشد Tokenهای صادرشده با کلید مشکوک نیز باطل شوند.

جزئیات دقیق این فرآیند به معماری Token و سیستم Revocation بستگی دارد.


# تفاوت Retire و Revoke

این دو مفهوم را نباید یکی بدانیم.

Retire

یعنی:

> از این Key برای Tokenهای جدید استفاده نکن.

اما ممکن است هنوز Public Key آن برای Verify کردن Tokenهای قدیمی منتشر شود.

Revoke

یعنی:

> این Key دیگر نباید معتبر تلقی شود.

در صورت Compromise ممکن است نیاز به Revocation سریع وجود داشته باشد.


# ساخت Key Manager

برای پروژه‌های بزرگ بهتر است مدیریت Key را در یک Service جدا قرار دهیم:


class KeyManager

{

    public function getActiveSigningKey(): array

    {

        // ...

    }



    public function createKey(): array

    {

        // ...

    }



    public function rotate(): void

    {

        // ...

    }



    public function getPublicKeys(): array

    {

        // ...

    }



    public function revoke(

        string $kid

    ): void {

        // ...

    }

}

Provider فقط از این Service استفاده می‌کند.


# فرآیند Rotate در KeyManager

به‌صورت مفهومی:


public function rotate(): void

{

    // 1. Generate new key



    // 2. Save key metadata



    // 3. Publish new public key



    // 4. Mark new key as active



    // 5. Mark old key as retiring

}

در Production این عملیات باید Transaction، Locking و کنترل خطا داشته باشد.


# Race Condition در Rotation

فرض کنید دو Process همزمان دستور Rotation را اجرا کنند:


Process A → Generate Key

Process B → Generate Key

ممکن است دو Key فعال ایجاد شوند.

برای جلوگیری از این وضعیت می‌توان از:


Database Transaction

Distributed Lock

Unique Constraints

Scheduled Job Lock

استفاده کرد.


# نمونه وضعیت کلیدها

فرض کنید Database:

kidstatus
rsa-2026-07revoked
rsa-2026-08retiring
rsa-2026-09active
rsa-2026-10published

در این حالت فقط:


active

برای Signing استفاده می‌شود.

اما:


retiring

ممکن است همچنان در JWKS منتشر شود.


# استفاده از Cron برای Rotation

می‌توان Rotation را به‌صورت Scheduled Job اجرا کرد.

مثلاً:


php artisan oidc:rotate-keys

در Laravel می‌توان آن را Scheduler کرد.

اما منطق اصلی Rotation باید مستقل از Scheduler باشد تا بتوانیم در شرایط اضطراری نیز آن را دستی اجرا کنیم.


# Rotation دوره‌ای

فرض کنید سیاست سازمان این باشد:


هر 90 روز

در این صورت Scheduler می‌تواند بررسی کند:


آخرین Rotation؟

       │

       ▼

بیش از 90 روز؟

       │

   ┌───┴───┐

  Yes      No

   │        │

Rotate    Nothing

عدد 90 روز یک مثال است و نباید به‌عنوان مقدار عمومی و الزامی برای همه پروژه‌ها در نظر گرفته شود.


# Algorithm Allowlist

حتی اگر JWKS شامل:


alg = RS256

باشد، Client باید الگوریتم‌های مورد قبول خودش را مشخص کند.

مثلاً:


Allowed:

RS256

و نباید هر الگوریتم دلخواهی را صرفاً به دلیل حضور در JWT قبول کنیم.


# استفاده از Public Key برای Verify

در معماری OIDC:


Provider

    │

    ├── Private Key

    │

    └── JWKS → Public Key

و Client:


JWKS

 ↓

Public Key

 ↓

JWT Verification

هیچ‌وقت نباید Private Key برای Client ارسال شود.


# JWKS را فقط از Issuer مورد انتظار دریافت کنید

Client نباید اجازه دهد URL مربوط به JWKS از یک ورودی غیرقابل اعتماد کاربر تعیین شود.

بهتر است:


Trusted Issuer

       ↓

Discovery

       ↓

jwks_uri

زنجیره اعتماد از Provider مورد انتظار آغاز شود.


# SSRF و Discovery

اگر Application اجازه دهد کاربر مقدار:


issuer=https://...

را تعیین کند، ممکن است زمینه‌ای برای SSRF ایجاد شود.

بنابراین در Applicationهای معمولی Issuer باید از Configuration امن Server تعیین شود.

مثلاً:


OIDC_ISSUER=https://auth.example.com

نه اینکه مستقیماً از:


$_GET['issuer']

خوانده شود.


# Public JWKS را با Private JWKS اشتباه نگیرید

معماری صحیح:


Public JWKS

    │

    └── Public Keys

در مقابل:


Private Key Store

    │

    └── Private Keys

این دو باید از نظر دسترسی کاملاً جدا باشند.


# تست Key Rotation

برای تست باید چند سناریو را بررسی کنیم.

سناریوی اول

Token با Old Key:


kid = old

و Old Key در JWKS موجود است.

باید:


Verify = Success

باشد.

سناریوی دوم

Token با New Key:


kid = new

و New Key در JWKS موجود است.

باید:


Verify = Success

باشد.

سناریوی سوم

Token با Key ناشناخته:


kid = unknown

باید رد شود.

سناریوی چهارم

Old Key از JWKS حذف شده و Token قدیمی هنوز معتبر است.

این سناریو باید با سیاست Rotation شما بررسی شود و نشان دهد که حذف کلید خیلی زود انجام نشده است.


# تست شکست Private Key

اگر Private Key فعلی در دسترس نباشد:


Signing

   ↓

Failure

Provider نباید Token جعلی یا بدون Signature معتبر تولید کند.

بهتر است سیستم:

  • خطا را ثبت کند
  • Alert ایجاد کند
  • از صدور Token ناامن جلوگیری کند.

# Monitoring برای Key Management

در سیستم Production بهتر است مواردی مانند:


Current kid

Key age

Last rotation

Rotation failures

JWKS availability

Unknown kid count

Signature validation failures

مانیتور شوند.

اگر تعداد:


Unknown kid

به‌صورت ناگهانی زیاد شود، می‌تواند نشانه مشکل در Rotation یا حتی یک رویداد امنیتی باشد.


# هشدارهای مهم

اگر این موارد را مشاهده کردید، باید بررسی کنید:


Private Key exposed

Unknown kid spike

JWKS unavailable

Repeated signature failures

Unexpected algorithm

Unexpected issuer

این موارد را نباید صرفاً به عنوان خطاهای عادی نادیده گرفت.


# ساختار نهایی معماری Key Management

در نهایت می‌توانیم سیستم را این‌گونه طراحی کنیم:


                    ┌──────────────────┐

                    │   Key Manager    │

                    └────────┬─────────┘

                             │

               ┌─────────────┼─────────────┐

               ▼             ▼             ▼

          Key Storage    Key Metadata   Rotation

               │

               ▼

          Private Keys

               │

               ▼

        OIDC Provider

               │

        Sign ID Token

               │

               ▼

             JWT

               │

               ▼

             Client

               │

               ▼

             JWKS

               │

               ▼

          Public Key

               │

               ▼

        Verify Signature


# چک‌لیست Key Rotation

قبل از Production این موارد را بررسی کنید:

  • [ ] Private Key در Public Directory نیست
  • [ ] Private Key داخل Git نیست
  • [ ] دسترسی Private Key محدود است
  • [ ] هر Key دارای kid است
  • [ ] Public Key در JWKS منتشر می‌شود
  • [ ] Private Key در JWKS منتشر نمی‌شود
  • [ ] Algorithm مشخص و Allowlist شده است
  • [ ] Active Key مشخص است
  • [ ] Retiring Key تعریف شده است
  • [ ] Key Rotation خودکار یا عملیاتی دارید
  • [ ] New Key قبل از استفاده در JWKS منتشر می‌شود
  • [ ] Old Key بلافاصله حذف نمی‌شود
  • [ ] Token Lifetime در Rotation لحاظ شده است
  • [ ] JWKS Cache در نظر گرفته شده است
  • [ ] Unknown kid مدیریت شده است
  • [ ] JWKS Refresh کنترل‌شده است
  • [ ] Rotation دارای Lock/Concurrency Control است
  • [ ] Emergency Rotation وجود دارد
  • [ ] Revocation با Retirement اشتباه گرفته نشده است
  • [ ] Monitoring برای Rotation وجود دارد
  • [ ] Private Key در Log قرار نمی‌گیرد
  • [ ] Tokenها در Log قرار نمی‌گیرند

# جمع‌بندی

مدیریت کلید یکی از قسمت‌هایی است که در طراحی JWT، OAuth 2.0 و OpenID Connect ممکن است در ابتدا ساده به نظر برسد، اما در Production اهمیت بسیار زیادی پیدا می‌کند.

مدل صحیح را می‌توان به این شکل خلاصه کرد:


Private Key

    ↓

Sign JWT

    ↓

kid

    ↓

JWT

و سمت Client:


JWT

 ↓

kid

 ↓

JWKS

 ↓

Public Key

 ↓

Signature Verification

اما مهم‌ترین قسمت زمانی است که می‌خواهیم کلید را تغییر دهیم:


Create New Key

       ↓

Publish New Public Key

       ↓

Activate New Key

       ↓

Old Key → Retiring

       ↓

Wait for old tokens/cache

       ↓

Remove Old Key

به این ترتیب می‌توانیم بدون اینکه Key Rotation باعث اختلال گسترده در Authentication کاربران شود، کلیدهای Signing را به‌صورت دوره‌ای تغییر دهیم.

همچنین باید به خاطر داشته باشیم که:

> kid فقط یک شناسه برای انتخاب کلید است؛ امنیت واقعی از حفاظت Private Key، اعتبارسنجی صحیح Signature، کنترل Algorithm، بررسی Issuer و Audience و طراحی صحیح چرخه عمر کلیدها حاصل می‌شود.

در مرحله بعدی، مسیر منطقی این کلاستر می‌تواند وارد بخش پیاده‌سازی SSO چنداپلیکیشنی با PHP شود؛ جایی که یک Identity Provider مرکزی را به چند Application متصل می‌کنیم و معماری کامل Login، Session، Account Linking، Logout و دسترسی بین سرویس‌ها را طراحی می‌کنیم.

نظرات (0)

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

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

مقالات مرتبط

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

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

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

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

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

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

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

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

۲۲ دقیقه