اگر در پروژههای PHP فقط یک سیستم Login ساده داشته باشیم، معمولاً Session و Cookie برای مدیریت ورود کاربران کافی هستند. اما زمانی که چندین وبسایت، اپلیکیشن یا سرویس مختلف داشته باشیم، نگهداری سیستم احراز هویت جداگانه برای هر پروژه میتواند پیچیده و پرهزینه شود.
اینجاست که مفهوم Single Sign-On یا SSO اهمیت پیدا میکند.
در یک معماری SSO، کاربر یکبار در یک سرویس مرکزی وارد حساب خود میشود و سپس میتواند به چندین برنامه مختلف دسترسی داشته باشد؛ بدون اینکه لازم باشد در هر برنامه دوباره نام کاربری و رمز عبور خود را وارد کند.
برای ساخت چنین سیستمی، OAuth 2.0 بهتنهایی کافی نیست؛ زیرا OAuth در اصل برای Authorization یا مجوز دسترسی طراحی شده است.
برای اینکه علاوه بر مجوز دسترسی، بتوانیم هویت کاربر را نیز استاندارد و قابل اعتماد منتقل کنیم، از OpenID Connect یا OIDC استفاده میکنیم.
در این مقاله میخواهیم یک OpenID Connect Provider را با PHP از پایه بررسی کنیم و اجزای مهم آن مانند:
- Discovery
- Authorization Endpoint
- Token Endpoint
- ID Token
- JWKS
- UserInfo
- Nonce
- PKCE
- SSO
- Logout
را بررسی کنیم.
> نکته مهم: هدف این مقاله آموزش معماری و پیادهسازی اجزای OIDC است. برای استفاده در یک سیستم Production واقعی، بهتر است بخشهای حساس OAuth/OIDC و JOSE/JWT بر پایه کتابخانههای استاندارد و تستشده پیادهسازی شوند و از نوشتن الگوریتمهای رمزنگاری بهصورت دستی خودداری شود.
OpenID Connect چیست؟
OpenID Connect که معمولاً با نام OIDC شناخته میشود، یک لایه احراز هویت روی OAuth 2.0 است.
به زبان ساده:
OAuth 2.0 میگوید:
> این برنامه اجازه دارد به این Resource دسترسی داشته باشد.
اما OpenID Connect علاوه بر آن میگوید:
> این کاربر چه کسی است؟
بنابراین میتوانیم رابطه زیر را در نظر بگیریم:
OAuth 2.0
+
Identity Layer
=
OpenID Connect
مهمترین تفاوت این دو در نوع اطلاعاتی است که منتقل میکنند.
در OAuth معمولاً با Access Token سروکار داریم.
در OIDC علاوه بر Access Token، یک ID Token نیز داریم که اطلاعات هویتی کاربر را در اختیار Client قرار میدهد.
# اجزای اصلی OpenID Connect
در یک سیستم OIDC معمولاً چند نقش اصلی داریم.
1. End User
کاربری که میخواهد وارد یک برنامه شود.
2. OpenID Provider
سروری که:
- کاربر را Login میکند
- هویت او را تأیید میکند
- Authorization Code صادر میکند
- Token صادر میکند
- ID Token تولید میکند
- اطلاعات UserInfo را ارائه میدهد
3. Client / Relying Party
برنامهای که Login را به OpenID Provider واگذار میکند.
برای مثال:
app.example.com
4. Resource Server
سرویسی که API یا Resource محافظتشده را ارائه میکند.
مثلاً:
api.example.com
# معماری پیشنهادی
فرض کنیم سه سرویس داریم:
auth.example.com
api.example.com
app.example.com
معماری میتواند به این شکل باشد:
┌─────────────────────┐
│ Browser │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ app.example.com │
│ Client │
└──────────┬──────────┘
│
Authorization
│
▼
┌─────────────────────┐
│ auth.example.com │
│ OpenID Provider │
│ │
│ Login │
│ Consent │
│ Authorization │
│ Token │
│ ID Token │
│ JWKS │
│ UserInfo │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ api.example.com │
│ Resource Server │
└─────────────────────┘
در این معماری، auth.example.com مرکز احراز هویت است.
# چرا OpenID Connect Provider بسازیم؟
فرض کنید سه سایت دارید:
shop.example.com
panel.example.com
blog.example.com
بدون SSO ممکن است هر سایت سیستم Login خودش را داشته باشد.
در نتیجه کاربر باید:
Shop Login
Panel Login
Blog Login
داشته باشد.
اما با OIDC:
┌───────────────┐
│ OpenID Provider│
└───────┬───────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
Shop Panel Blog
کاربر میتواند یک حساب مرکزی داشته باشد.
این مدل برای:
- SaaS
- Marketplace
- پنلهای مدیریتی
- سرویسهای سازمانی
- چند اپلیکیشن مرتبط
- سیستمهای چنددامنهای
کاربرد زیادی دارد.
# پیشنیازهای پیادهسازی
برای نمونههای این مقاله پیشنهاد میکنیم از موارد زیر استفاده کنید:
PHP 8+
MySQL / PostgreSQL
Composer
HTTPS
در محیط توسعه میتوانید از HTTP استفاده کنید، اما در محیط Production احراز هویت و Token Endpoint باید روی HTTPS باشند.
# ساختار پیشنهادی پروژه
یک ساختار ساده میتواند به این شکل باشد:
oidc-provider/
│
├── config/
│ └── oidc.php
│
├── src/
│ ├── Auth/
│ ├── OAuth/
│ ├── OIDC/
│ ├── Token/
│ └── Security/
│
├── public/
│ ├── oauth/
│ │ ├── authorize.php
│ │ └── token.php
│ │
│ ├── oidc/
│ │ ├── userinfo.php
│ │ └── logout.php
│ │
│ └── .well-known/
│ ├── openid-configuration.php
│ └── jwks.json.php
│
└── vendor/
این ساختار باعث میشود Endpointهای OAuth و OIDC از منطق داخلی برنامه جدا باشند.
# دیتابیس OpenID Connect
برای شروع میتوانیم از جدولهای OAuth مقاله قبلی استفاده کنیم.
برای مثال:
users
oauth_clients
oauth_authorization_codes
oauth_access_tokens
oauth_refresh_tokens
جدول کاربران:
CREATE TABLE users (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255) NULL,
email_verified_at DATETIME NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
برای Client:
CREATE TABLE oauth_clients (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
client_id VARCHAR(100) NOT NULL UNIQUE,
client_secret_hash VARCHAR(255) NULL,
name VARCHAR(255) NOT NULL,
redirect_uris TEXT NOT NULL,
created_at DATETIME NOT NULL
);
برای Authorization Code:
CREATE TABLE oauth_authorization_codes (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
code_hash VARCHAR(255) NOT NULL,
client_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
redirect_uri TEXT NOT NULL,
scope TEXT NOT NULL,
nonce VARCHAR(255) NULL,
code_challenge VARCHAR(255) NULL,
code_challenge_method VARCHAR(20) NULL,
expires_at DATETIME NOT NULL,
used_at DATETIME NULL,
created_at DATETIME NOT NULL
);
نکته مهم این است که Authorization Code باید:
- کوتاهعمر باشد
- یکبار مصرف باشد
- به Client متصل باشد
- به Redirect URI متصل باشد
- در صورت استفاده از PKCE به Code Challenge متصل باشد.
# Discovery در OpenID Connect چیست؟
یکی از قابلیتهای مهم OIDC، Discovery است.
Client به جای اینکه تمام Endpointهای Provider را از قبل بداند، میتواند اطلاعات Provider را از آدرس استاندارد زیر دریافت کند:
/.well-known/openid-configuration
مثلاً:
https://auth.example.com/.well-known/openid-configuration
نمونه Response:
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/oauth/authorize",
"token_endpoint": "https://auth.example.com/oauth/token",
"userinfo_endpoint": "https://auth.example.com/oidc/userinfo",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code",
"refresh_token"
],
"scopes_supported": [
"openid",
"profile",
"email"
],
"id_token_signing_alg_values_supported": [
"RS256"
]
}
# ساخت Discovery Endpoint در PHP
فایل:
public/.well-known/openid-configuration.php
میتواند چنین چیزی باشد:
<?php
header('Content-Type: application/json');
$issuer = 'https://auth.example.com';
echo json_encode([
'issuer' => $issuer,
'authorization_endpoint' =>
$issuer . '/oauth/authorize',
'token_endpoint' =>
$issuer . '/oauth/token',
'userinfo_endpoint' =>
$issuer . '/oidc/userinfo',
'jwks_uri' =>
$issuer . '/.well-known/jwks.json',
'response_types_supported' => [
'code'
],
'grant_types_supported' => [
'authorization_code',
'refresh_token'
],
'scopes_supported' => [
'openid',
'profile',
'email'
],
'id_token_signing_alg_values_supported' => [
'RS256'
]
], JSON_UNESCAPED_SLASHES);
در پروژه واقعی مقدار issuer را بهتر است از Configuration بخوانید و به شکل ناخواسته از Host درخواست تولید نکنید.
# Authorization Endpoint
Authorization Endpoint یکی از مهمترین قسمتهای Provider است.
Client کاربر را به این Endpoint منتقل میکند:
/oauth/authorize
یک Request نمونه:
GET /oauth/authorize
?response_type=code
&client_id=client123
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=openid%20profile%20email
&state=abc123
&nonce=xyz789
&code_challenge=...
&code_challenge_method=S256
پارامترهای مهم عبارتاند از:
response_type
برای Authorization Code Flow:
code
client_id
شناسه Client.
redirect_uri
آدرسی که کاربر بعد از Authorization به آن برمیگردد.
scope
مثلاً:
openid profile email
وجود openid مشخص میکند که Request مربوط به OIDC است.
state
برای جلوگیری از حملات CSRF و اتصال Response به Request اصلی استفاده میشود.
nonce
برای ارتباط دادن ID Token با درخواست Authentication استفاده میشود.
code_challenge
برای PKCE استفاده میشود.
# چرا Nonce مهم است؟
Nonce یک مقدار تصادفی است که Client ایجاد میکند.
مثلاً:
$nonce = bin2hex(random_bytes(32));
Client آن را در Authorization Request قرار میدهد.
Provider نیز باید آن را در فرآیند Authentication نگهداری کند و در ID Token قرار دهد.
در نهایت Client هنگام اعتبارسنجی ID Token بررسی میکند:
nonce موجود در ID Token
=
nonce ایجادشده برای Request
اگر برابر نباشند، Token نباید پذیرفته شود.
# Login در Authorization Endpoint
وقتی کاربر وارد /oauth/authorize میشود، Provider باید بررسی کند:
آیا Session دارد؟
│
خیر
│
▼
Login
│
▼
Session
│
▼
Consent
اگر کاربر قبلاً Login کرده باشد، ممکن است مستقیماً به مرحله Consent برسد.
# Consent چیست؟
Consent صفحهای است که Provider به کاربر نشان میدهد برنامه درخواست چه دسترسیهایی دارد.
مثلاً:
برنامه Example درخواست دسترسی دارد:
✓ مشاهده نام شما
✓ مشاهده ایمیل شما
[تأیید] [لغو]
این قسمت بهخصوص در سیستمهای عمومی OAuth/OIDC اهمیت زیادی دارد.
# تولید Authorization Code
بعد از اینکه کاربر Login شد و Authorization را تأیید کرد، Provider باید یک Authorization Code ایجاد کند.
مثلاً:
$code = bin2hex(random_bytes(32));
اما بهتر است مقدار خام Code در دیتابیس ذخیره نشود.
میتوانیم Hash آن را ذخیره کنیم:
$codeHash = hash('sha256', $code);
و Code خام فقط برای Redirect استفاده شود.
مثلاً:
https://app.example.com/callback
?code=RANDOM_CODE
&state=abc123
# چرا Authorization Code باید کوتاهعمر باشد؟
Authorization Code نباید مانند Access Token مدت زیادی معتبر باشد.
مثلاً میتوانیم زمان انقضای کوتاهی در نظر بگیریم:
Code lifetime:
60 ثانیه
همچنین Code باید:
One-Time Use
باشد.
یعنی بعد از اولین استفاده دیگر معتبر نباشد.
# Token Endpoint
Client بعد از دریافت Code آن را به Token Endpoint ارسال میکند:
POST /oauth/token
نمونه:
grant_type=authorization_code
client_id=client123
code=...
redirect_uri=https://app.example.com/callback
code_verifier=...
Provider باید موارد مختلفی را بررسی کند:
Code وجود دارد؟
↓
منقضی نشده؟
↓
قبلاً استفاده نشده؟
↓
Client صحیح است؟
↓
Redirect URI صحیح است؟
↓
PKCE معتبر است؟
↓
User معتبر است؟
↓
صدور Token
# Access Token و ID Token
در OIDC ممکن است Response چیزی شبیه این باشد:
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "..."
}
اینجا دو Token مهم داریم.
Access Token
برای دسترسی به Resource Server استفاده میشود.
مثلاً:
GET /api/profile
Authorization: Bearer ACCESS_TOKEN
ID Token
برای انتقال اطلاعات هویتی Authentication به Client استفاده میشود.
این دو را نباید با یکدیگر اشتباه بگیریم.
# ID Token چیست؟
ID Token معمولاً یک JWT است.
ساختار JWT:
HEADER.PAYLOAD.SIGNATURE
مثلاً:
xxxxx.yyyyy.zzzzz
Header میتواند شامل مواردی مانند:
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2026-01"
}
Payload میتواند شامل:
{
"iss": "https://auth.example.com",
"sub": "user_123",
"aud": "client123",
"iat": 1780000000,
"exp": 1780003600,
"nonce": "..."
}
# مهمترین Claimهای ID Token
iss
Issuer را مشخص میکند:
https://auth.example.com
Client باید آن را بررسی کند.
sub
شناسه یکتای کاربر در Provider است.
مثلاً:
7f2a9c...
بهتر است Email کاربر را به عنوان sub استفاده نکنیم.
زیرا Email ممکن است تغییر کند.
aud
Audience مشخص میکند Token برای کدام Client صادر شده است.
مثلاً:
client123
Client باید حتماً aud را بررسی کند.
iat
زمان ایجاد Token.
exp
زمان انقضا.
nonce
Nonce مربوط به Authentication Request.
Client باید آن را با مقدار اصلی مقایسه کند.
# اطلاعات پروفایل در ID Token
بسته به Scope و طراحی Provider ممکن است Claimهایی مانند:
{
"email": "user@example.com",
"email_verified": true,
"name": "Ali"
}
نیز وجود داشته باشند.
اما بهتر است فقط اطلاعاتی را در ID Token قرار دهیم که برای Authentication Client لازم هستند.
برای اطلاعات بیشتر میتوان از UserInfo Endpoint استفاده کرد.
# چرا نباید JWT را فقط Base64 Decode کنیم؟
یکی از اشتباهات خطرناک این است که توسعهدهنده JWT را Decode کند و Payload را معتبر فرض کند.
مثلاً:
base64 decode
↓
payload
↓
اعتماد
این روش اعتبار Token را اثبات نمیکند.
JWT باید از نظر مواردی مانند:
Signature
Issuer
Audience
Expiration
Nonce
اعتبارسنجی شود.
# امضای ID Token با RSA
در یک Provider واقعی بهتر است از رمزنگاری نامتقارن استفاده کنیم.
مدل کلی:
Private Key
│
▼
Sign ID Token
│
▼
JWT
و Client برای بررسی:
JWKS
│
▼
Public Key
│
▼
Verify Signature
مزیت این معماری این است که Private Key فقط نزد Provider باقی میماند.
# Private Key را هرگز Public نکنید
ساختار باید چیزی شبیه این باشد:
Provider
private.key
│
└── فقط روی Server
public key
│
└── JWKS Endpoint
اگر Private Key افشا شود، مهاجم بالقوه میتواند Tokenهای جعلی تولید کند.
بنابراین Private Key نباید:
- داخل Git قرار بگیرد
- داخل Repository عمومی باشد
- در Frontend قرار بگیرد
- در JavaScript ارسال شود
- در Log نوشته شود
# JWKS چیست؟
JWKS مخفف:
JSON Web Key Set
است.
Provider کلیدهای عمومی خود را در یک Endpoint منتشر میکند:
/.well-known/jwks.json
مثلاً:
{
"keys": [
{
"kty": "RSA",
"kid": "key-2026-01",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}
# kid چیست؟
kid مخفف Key ID است.
فرض کنید Provider دو کلید دارد:
key-2026-01
key-2026-06
JWT مشخص میکند با کدام کلید امضا شده:
{
"alg": "RS256",
"kid": "key-2026-06"
}
Client از JWKS کلید مربوط به همین kid را پیدا میکند.
# چرا Key Rotation مهم است؟
نباید یک Private Key را برای همیشه نگه داریم.
میتوانیم بهصورت دورهای کلیدها را Rotate کنیم:
Old Key
│
▼
New Key
اما کلید قدیمی را فوراً حذف نمیکنیم.
برای مدتی:
JWKS
Old Public Key
New Public Key
هر دو منتشر میشوند تا Tokenهای قدیمی فرصت اعتبارسنجی داشته باشند.
بعد از منقضی شدن Tokenهای قدیمی، کلید قبلی حذف میشود.
# تبدیل RSA Public Key به JWK
برای انتشار کلید RSA در JWKS باید اطلاعاتی مانند:
kty
kid
use
alg
n
e
ارائه شود.
برای مثال:
[
'kty' => 'RSA',
'kid' => 'key-2026-01',
'use' => 'sig',
'alg' => 'RS256',
'n' => $modulus,
'e' => $exponent,
]
تبدیل دقیق RSA Key به JWK شامل Encoding صحیح پارامترهای کلید است.
در پروژه Production بهتر است این قسمت را به یک کتابخانه معتبر JOSE/JWK بسپارید و الگوریتم Encoding را دستی پیادهسازی نکنید.
# UserInfo Endpoint
یکی دیگر از Endpointهای OIDC:
/oidc/userinfo
است.
Client میتواند Access Token را ارسال کند:
GET /oidc/userinfo
Authorization: Bearer ACCESS_TOKEN
Provider Token را اعتبارسنجی میکند و اطلاعات کاربر را برمیگرداند.
مثلاً:
{
"sub": "user_123",
"name": "Ali",
"email": "user@example.com",
"email_verified": true
}
# Scope در UserInfo
اطلاعاتی که Client دریافت میکند باید به Scopeهای مجاز وابسته باشد.
مثلاً:
openid
profile
email
اگر Client فقط:
openid email
درخواست کرده باشد، Provider نباید تمام اطلاعات پروفایل کاربر را بدون دلیل در اختیار آن قرار دهد.
# تفاوت ID Token و UserInfo
بهصورت ساده:
| مورد | ID Token | UserInfo |
|---|---|---|
| هدف | Authentication | اطلاعات کاربر |
| نوع رایج | JWT | JSON |
| صادرکننده | Provider | Provider |
| ارسال معمول | Token Response | API Request |
| نیاز به Access Token | خیر | بله |
| Signature | معمولاً دارد | معمولاً پاسخ API است |
# شناسه کاربر را با Email یکی نکنید
فرض کنید کاربر:
ali@example.com
است.
اگر Email را به عنوان شناسه اصلی OIDC قرار دهیم و بعد کاربر Email خود را تغییر دهد، ممکن است Mapping حسابها دچار مشکل شود.
بهتر است:
issuer + sub
به عنوان Identity اصلی ذخیره شود.
مثلاً جدول:
CREATE TABLE user_identities (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
issuer VARCHAR(255) NOT NULL,
subject VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL,
UNIQUE KEY unique_oidc_identity (issuer, subject)
);
بنابراین:
https://auth.example.com
+
user_123
یک Identity مشخص را تشکیل میدهد.
# Single Sign-On چگونه کار میکند؟
فرض کنید کاربر میخواهد وارد:
app1.example.com
شود.
App1 کاربر را به Provider میفرستد:
app1
│
▼
auth.example.com
اگر کاربر Login نباشد:
Login
│
▼
Provider Session
بعد از Login:
Authorization Code
│
▼
app1
حالا کاربر وارد App1 است.
اگر بعداً وارد:
app2.example.com
شود:
app2
│
▼
auth.example.com
│
│ Provider Session exists
▼
Authorization Code
│
▼
app2
کاربر ممکن است دیگر Login فرم را مشاهده نکند.
این همان مفهوم SSO است.
# Session Provider و Session Application یکی نیستند
یک نکته معماری بسیار مهم:
Provider Session
با:
Application Session
یکی نیست.
Provider Session میگوید:
> کاربر در Identity Provider وارد شده است.
Application Session میگوید:
> کاربر در این برنامه خاص وارد شده است.
بنابراین هر Application باید Session خودش را مدیریت کند.
# Logout در OIDC
Logout در سیستم SSO کمی پیچیدهتر از Login است.
ممکن است کاربر از:
app.example.com
Logout کند، اما همچنان در:
auth.example.com
Login باشد.
در این حالت اگر دوباره Login کند، ممکن است بدون وارد کردن Password دوباره وارد شود.
برای Logout کامل باید معماری Logout را مشخص کنیم.
مواردی مانند:
Local Logout
Provider Logout
RP-Initiated Logout
Post Logout Redirect
میتوانند در این فرآیند نقش داشته باشند.
# Post Logout Redirect URI
اگر بعد از Logout قرار است کاربر به URL خاصی منتقل شود، آن URL نیز باید Allowlist شود.
مثلاً:
https://app.example.com/logout/callback
نباید اجازه دهیم کاربر یک URL دلخواه ارسال کند.
در غیر این صورت ممکن است Open Redirect ایجاد شود.
# PKCE را جدی بگیرید
PKCE برای Authorization Code Flow یکی از مهمترین مکانیزمهای امنیتی است.
Client ابتدا:
code_verifier
میسازد.
سپس:
code_challenge =
BASE64URL(
SHA256(code_verifier)
)
را ارسال میکند.
Provider فقط code_challenge را نگه میدارد.
بعد Client در Token Request:
code_verifier
را ارسال میکند.
Provider دوباره آن را محاسبه میکند:
SHA256(code_verifier)
↓
code_challenge
و با مقدار ذخیرهشده مقایسه میکند.
# چرا Redirect URI باید دقیق باشد؟
یکی از اشتباهات خطرناک:
https://example.com/*
یا:
https://example.com/callback?
به شکل بیش از حد آزاد است.
بهتر است Redirect URI دقیقاً Allowlist شود:
https://app.example.com/oauth/callback
و مقدار Request دقیقاً با مقدار ثبتشده مقایسه شود.
# بررسیهای امنیتی Authorization Endpoint
در Authorization Endpoint بهتر است موارد زیر بررسی شوند:
client_id
redirect_uri
response_type
scope
state
nonce
code_challenge
code_challenge_method
و همچنین:
Client فعال است؟
Scope مجاز است؟
Redirect URI ثبت شده؟
کاربر Login است؟
کاربر Consent داده؟
# جلوگیری از Authorization Code Replay
Authorization Code باید یکبار مصرف باشد.
مثلاً هنگام استفاده:
UPDATE oauth_authorization_codes
SET used_at = NOW()
WHERE id = ?
AND used_at IS NULL
AND expires_at > NOW();
در سیستمهای همزمان بهتر است از Transaction و Lock مناسب استفاده شود تا دو Request نتوانند همزمان یک Code را مصرف کنند.
# Token Lifetime
Access Token نباید برای مدت نامحدود معتبر باشد.
مثلاً:
Access Token:
60 دقیقه
و Refresh Token میتواند عمر طولانیتری داشته باشد.
اما Refresh Token نیز باید:
- قابل Revocation باشد
- در جای امن نگهداری شود
- در صورت امکان Rotate شود
- در Log نوشته نشود.
# Refresh Token Rotation
یک معماری امنتر:
Refresh Token A
│
▼
Token Endpoint
│
├── Access Token B
│
└── Refresh Token C
Refresh Token قبلی:
Refresh Token A
دیگر معتبر نیست.
اگر یک Refresh Token قدیمی دوباره استفاده شود، میتوان آن را به عنوان نشانه احتمالی سرقت Token بررسی کرد و Session/Token Family مربوطه را Revoked کرد.
# ساخت ID Token در PHP
در سطح مفهومی، دادهای شبیه این تولید میکنیم:
$claims = [
'iss' => 'https://auth.example.com',
'sub' => $user->id,
'aud' => $clientId,
'iat' => time(),
'exp' => time() + 3600,
'nonce' => $nonce,
];
سپس این Claims باید با Private Key امضا شوند.
اما نباید صرفاً با چند تابع ساده PHP یک JWT Production ایجاد کنیم.
برای JWT/OIDC از یک کتابخانه معتبر و تستشده JOSE استفاده کنید.
# اعتبارسنجی ID Token در Client
Client باید حداقل موارد زیر را بررسی کند:
1. Signature
2. Algorithm
3. kid
4. iss
5. aud
6. exp
7. iat
8. nonce
در صورت استفاده از قابلیتهای مربوطه، مواردی مانند:
auth_time
max_age
acr
amr
نیز باید مطابق سناریوی پیادهسازی بررسی شوند.
# Algorithm Confusion
نباید به مقدار alg موجود در JWT بهصورت کورکورانه اعتماد کنیم.
Provider باید الگوریتمهای مجاز خود را مشخص کند.
مثلاً:
RS256
و Client نیز باید فقط الگوریتمهای مورد انتظار را قبول کند.
نباید اجازه دهیم Token با الگوریتمی غیرمنتظره وارد مسیر اعتبارسنجی شود.
# Security Checklist
قبل از Production، موارد زیر را بررسی کنید.
HTTPS
تمام Endpointهای Authentication و Token باید روی HTTPS باشند.
Redirect URI
Redirect URI باید دقیقاً Allowlist شود.
State
در Authorization Request استفاده شود.
Nonce
در OIDC Authentication استفاده و در ID Token بررسی شود.
PKCE
برای Authorization Code Flow استفاده شود.
Short-lived Authorization Code
Code باید کوتاهعمر و یکبارمصرف باشد.
Token Validation
فقط Decode کردن JWT کافی نیست.
Issuer Validation
مقدار:
iss
باید بررسی شود.
Audience Validation
مقدار:
aud
باید بررسی شود.
Signature Validation
امضای JWT باید واقعاً Verify شود.
Private Key Protection
Private Key هرگز نباید Public شود.
JWKS
Public Keyها از طریق JWKS ارائه شوند.
Key Rotation
برای کلیدهای Signing مکانیزم Rotation داشته باشید.
Rate Limiting
Endpointهای حساس مانند:
/login
/authorize
/token
باید Rate Limit داشته باشند.
Logging
این موارد را Log نکنید:
Password
Access Token
Refresh Token
Authorization Code
OTP
Client Secret
Private Key
# اشتباهات رایج در ساخت OIDC Provider
اشتباه اول: استفاده از Access Token به جای ID Token
Access Token برای دسترسی به Resource است.
ID Token برای اطلاعات Authentication است.
اشتباه دوم: اعتماد به Payload بدون بررسی Signature
Decode کردن JWT به معنی معتبر بودن آن نیست.
اشتباه سوم: بررسی نکردن Audience
ممکن است Token معتبر باشد اما برای Client دیگری صادر شده باشد.
اشتباه چهارم: بررسی نکردن Issuer
Client باید مطمئن شود Token واقعاً توسط Provider مورد انتظار صادر شده است.
اشتباه پنجم: استفاده از Email به عنوان Subject
Email ممکن است تغییر کند.
از یک sub پایدار استفاده کنید.
اشتباه ششم: استفاده نکردن از Nonce
Nonce یکی از مکانیزمهای مهم OIDC برای اتصال ID Token به Authentication Request است.
اشتباه هفتم: Redirect URI آزاد
هرگز اجازه ندهید Client مقصد Redirect را به شکل دلخواه تعیین کند.
اشتباه هشتم: ذخیره Private Key در Git
Private Key باید خارج از Repository عمومی و با کنترل دسترسی مناسب نگهداری شود.
اشتباه نهم: ساخت JWT با رمزنگاری دستی
پیادهسازی دستی رمزنگاری و JOSE میتواند باعث ایجاد آسیبپذیریهای جدی شود.
از کتابخانههای معتبر و تستشده استفاده کنید.
# تفاوت OAuth 2.0 Authorization Server و OpenID Provider
تا اینجا دو مفهوم را کنار هم دیدهایم.
OAuth 2.0 Authorization Server مسئول مواردی مانند:
Authorization
Access Token
Refresh Token
Scopes
Client
است.
OpenID Provider علاوه بر قابلیتهای OAuth، لایه Identity را نیز اضافه میکند:
ID Token
Nonce
UserInfo
Discovery
JWKS
Authentication
بنابراین:
OAuth 2.0
│
▼
Authorization Server
│
│ + Identity
▼
OpenID Connect
│
▼
OpenID Provider
# آیا باید OIDC Provider را از صفر بسازیم؟
برای یادگیری، بله.
اما برای Production باید با احتیاط بسیار بیشتری تصمیم گرفت.
OAuth 2.0 و OpenID Connect دارای جزئیات امنیتی زیادی هستند و یک اشتباه کوچک در:
Token Validation
Redirect URI
PKCE
Nonce
JWT
Key Management
Session
Consent
میتواند کل سیستم Authentication را تحت تأثیر قرار دهد.
بنابراین برای محصول واقعی بهتر است از راهکارها و کتابخانههای معتبر و تستشده استفاده شود و کدهای حساس تا حد امکان از نو اختراع نشوند.
# معماری نهایی پیشنهادی
در نهایت معماری Provider میتواند چیزی شبیه این باشد:
Browser
│
▼
┌─────────────────┐
│ Application │
│ Relying Party │
└────────┬────────┘
│
Authorization
│
▼
┌──────────────────────┐
│ OpenID Provider │
│ │
│ Login │
│ Consent │
│ Authorization │
│ Token │
│ ID Token │
│ UserInfo │
│ Discovery │
│ JWKS │
│ Logout │
└──────────┬───────────┘
│
┌─────────┴─────────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ Database │ │ Key Store │
└───────────┘ └───────────┘
# چکلیست نهایی ساخت OpenID Connect Provider
قبل از اینکه Provider را Production کنید:
- [ ] HTTPS فعال است
- [ ] Discovery Endpoint ایجاد شده
- [ ] Authorization Endpoint ایجاد شده
- [ ] Token Endpoint ایجاد شده
- [ ] UserInfo Endpoint ایجاد شده
- [ ] JWKS Endpoint ایجاد شده
- [ ] Authorization Code یکبارمصرف است
- [ ] Authorization Code کوتاهعمر است
- [ ] PKCE پیادهسازی شده
- [ ] State بررسی میشود
- [ ] Nonce بررسی میشود
- [ ] Redirect URI Allowlist شده
- [ ] ID Token با کلید نامتقارن امضا میشود
- [ ] Signature اعتبارسنجی میشود
- [ ] Issuer بررسی میشود
- [ ] Audience بررسی میشود
- [ ] Expiration بررسی میشود
- [ ] Subject پایدار است
- [ ] Private Key محافظت شده
- [ ] JWKS دارای
kidاست - [ ] Key Rotation در نظر گرفته شده
- [ ] Refresh Token Rotation در نظر گرفته شده
- [ ] Rate Limiting فعال است
- [ ] Tokenها در Log ذخیره نمیشوند
- [ ] Session امنیتی است
- [ ] Logout به شکل صحیح طراحی شده
- [ ] Consent در صورت نیاز پیادهسازی شده
# جمعبندی
OpenID Connect یکی از مهمترین استانداردها برای ساخت سیستمهای Authentication مدرن است.
OAuth 2.0 به ما امکان مدیریت Authorization را میدهد، اما OIDC یک لایه استاندارد برای Identity و Authentication روی آن اضافه میکند.
در یک OpenID Connect Provider باید اجزایی مانند:
Discovery
Authorization Endpoint
Token Endpoint
ID Token
JWKS
UserInfo
Nonce
PKCE
Session
SSO
Logout
را در کنار یکدیگر طراحی کنیم.
یکی از مهمترین نکات این معماری نیز این است که ID Token را با Access Token اشتباه نگیریم و هنگام اعتبارسنجی JWT صرفاً به Decode کردن Payload اکتفا نکنیم.
در یک سیستم واقعی، Signature، Issuer، Audience، Expiration، Nonce و سایر Claims موردنیاز باید بهصورت دقیق اعتبارسنجی شوند.
در نهایت اگر هدف شما ساخت یک اکوسیستم چندسرویسی باشد، OIDC میتواند پایهای استاندارد برای ایجاد:
یک حساب کاربری
↓
یک Login مرکزی
↓
چند Application
↓
SSO
فراهم کند.
در مقاله بعدی میتوانیم یک مرحله عملیتر جلو برویم و به سراغ پیادهسازی SSO با OpenID Connect در PHP برویم؛ یعنی یک OIDC Client واقعی بسازیم که به Provider متصل شود، Authorization Code + PKCE را اجرا کند، ID Token را اعتبارسنجی کند، کاربر را به حساب داخلی متصل کند و Session محلی بسازد.





