در سیستمهایی که از 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:
| kid | status |
|---|---|
| rsa-2026-07 | revoked |
| rsa-2026-08 | retiring |
| rsa-2026-09 | active |
| rsa-2026-10 | published |
در این حالت فقط:
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 و دسترسی بین سرویسها را طراحی میکنیم.





