مقدمه
فرض کنید یک مجموعه چند وبسایت و اپلیکیشن مختلف دارد:
auth.example.comportal.example.comshop.example.comadmin.example.comapi.example.com
کاربر وارد portal.example.com میشود و با حساب کاربری خود لاگین میکند. بعد از آن، وقتی وارد shop.example.com یا admin.example.com میشود، دیگر لازم نیست دوباره نام کاربری و رمز عبور خود را وارد کند.
این قابلیت Single Sign-On یا به اختصار SSO نام دارد.
SSO یکی از کاربردهای مهم OpenID Connect است و در معماریهای چنداپلیکیشنی میتواند سیستم احراز هویت را از خود Applicationها جدا کند.
در این مقاله با یک معماری واقعی، مرحلهبهمرحله بررسی میکنیم که چطور میتوان چنین سیستمی را با PHP پیادهسازی کرد.
# SSO چیست؟
SSO مخفف عبارت:
Single Sign-On
است.
معنای آن این است که کاربر یک بار در یک سیستم مرکزی احراز هویت میشود و سپس میتواند از همان هویت برای ورود به چند Application مختلف استفاده کند.
برای مثال:
┌──────────────────────┐
│ Identity Provider │
│ auth.example.com │
└──────────┬───────────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Portal │ │ Shop │ │ Admin │
│ App │ │ App │ │ App │
└──────────┘ └──────────┘ └──────────┘
در این معماری، auth.example.com مسئول اصلی احراز هویت است.
Applicationهای دیگر به جای اینکه سیستم Login مستقل و کاملاً جداگانه داشته باشند، از Identity Provider مرکزی استفاده میکنند.
# SSO چه مشکلی را حل میکند؟
فرض کنید سه Application داریم:
portal.example.com
shop.example.com
admin.example.com
بدون SSO ممکن است هرکدام سیستم Login جداگانه داشته باشند:
Portal → Username/Password
Shop → Username/Password
Admin → Username/Password
در نتیجه کاربر باید:
- چند حساب داشته باشد
- چند رمز عبور مدیریت کند
- چند بار Login کند
- چند سیستم Reset Password داشته باشد
اما در معماری SSO:
Auth Server
│
┌──────────┼──────────┐
▼ ▼ ▼
Portal Shop Admin
احراز هویت در یک نقطه مرکزی انجام میشود.
# SSO با Session مشترک یکی نیست
یکی از اشتباهات رایج این است که تصور کنیم برای ساخت SSO باید Cookie مربوط به Session را بین همه Applicationها به اشتراک بگذاریم.
مثلاً:
.example.com
را برای Cookie Domain قرار دهیم و Session را بین سایتها مشترک کنیم.
این روش در برخی معماریهای خاص ممکن است قابل استفاده باشد، اما برای یک معماری استاندارد چنداپلیکیشنی، روش اصلی و قابل توسعهای نیست.
در SSO مبتنی بر OpenID Connect، هر Application Session خودش را دارد.
مثلاً:
Portal
└── Session خودش
Shop
└── Session خودش
Admin
└── Session خودش
اما همه این Applicationها به یک Identity Provider مرکزی اعتماد میکنند.
# معماری SSO با OpenID Connect
یک معماری نمونه را در نظر بگیریم:
auth.example.com
Identity Provider ما است.
سه Application داریم:
portal.example.com
shop.example.com
admin.example.com
و یک API:
api.example.com
ساختار کلی:
┌───────────────────┐
│ Identity Provider │
│ auth.example.com │
└─────────┬─────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Portal │ │ Shop │ │ Admin │
│ App │ │ App │ │ App │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└────────────────────┼────────────────────┘
▼
┌──────────────┐
│ API │
│ api.example │
└──────────────┘
# اجزای اصلی سیستم
در این معماری چند بخش اصلی داریم.
1. Identity Provider
Identity Provider یا IdP سیستم مرکزی احراز هویت است.
وظایف آن شامل:
- Login
- Logout
- مدیریت کاربران
- مدیریت Session احراز هویت
- صدور Authorization Code
- صدور ID Token
- صدور Access Token
- ارائه UserInfo
- انتشار JWKS
- مدیریت Clientها
- مدیریت Scopeها
است.
# 2. OIDC Client
هر Application که میخواهد از سیستم مرکزی استفاده کند، یک OIDC Client است.
برای مثال:
Portal Client
Shop Client
Admin Client
هرکدام میتوانند Client ID مخصوص خودشان را داشته باشند.
مثلاً:
portal-web
shop-web
admin-web
# 3. Resource Server
API میتواند Resource Server باشد.
مثلاً:
api.example.com
Access Token را دریافت میکند و بررسی میکند که آیا کاربر اجازه دسترسی به Resource موردنظر را دارد یا خیر.
# SSO دقیقاً چگونه کار میکند؟
فرآیند را مرحلهبهمرحله بررسی کنیم.
کاربر وارد این آدرس میشود:
https://portal.example.com
Application متوجه میشود کاربر Login نیست.
بنابراین او را به Identity Provider میفرستد:
https://auth.example.com/authorize
با پارامترهایی مانند:
client_id
redirect_uri
response_type=code
scope=openid profile email
state
nonce
code_challenge
code_challenge_method=S256
# مرحله Login در Identity Provider
کاربر وارد:
auth.example.com
میشود.
اگر Session معتبر نداشته باشد، فرم Login را مشاهده میکند:
Email
Password
[ Login ]
بعد از ورود موفق، Identity Provider یک Authorization Code ایجاد میکند.
مثلاً:
AUTHORIZATION_CODE_123
سپس کاربر را به Application برمیگرداند:
https://portal.example.com/callback?code=...&state=...
# Application چه کاری انجام میدهد؟
Application باید Authorization Code را به Token تبدیل کند.
درخواستی مانند:
POST https://auth.example.com/token
ارسال میشود.
پارامترهای اصلی:
grant_type=authorization_code
code=...
redirect_uri=https://portal.example.com/callback
client_id=...
client_secret=...
code_verifier=...
Identity Provider پاسخ میدهد:
{
"access_token": "....",
"id_token": "....",
"token_type": "Bearer",
"expires_in": 3600
}
# ID Token چیست؟
ID Token برای انتقال اطلاعات هویتی کاربر به Client استفاده میشود.
مثلاً:
{
"iss": "https://auth.example.com",
"sub": "8f4c...",
"aud": "portal-web",
"email": "user@example.com",
"email_verified": true,
"name": "Ali Ahmadi",
"iat": 1780000000,
"exp": 1780003600
}
در اینجا:
iss
مشخص میکند Token توسط چه Identity Providerای صادر شده است.
sub
شناسه پایدار کاربر نزد همان Provider است.
aud
مشخص میکند Token برای کدام Client صادر شده است.
# Access Token چیست؟
Access Token معمولاً برای دسترسی به API استفاده میشود.
مثلاً:
GET /api/orders
Authorization: Bearer ACCESS_TOKEN
بنابراین باید این دو مفهوم را از هم جدا کنیم:
ID Token
↓
اطلاعات هویت کاربر برای Client
Access Token
↓
دسترسی به Resource Server / API
نباید صرفاً به این دلیل که یک Access Token معتبر است، آن را بهعنوان مدرک هویت کاربر در Client استفاده کنیم.
# چرا وقتی وارد Application دوم میشویم دوباره Login نمیکنیم؟
این قسمت مهمترین بخش SSO است.
فرض کنید کاربر قبلاً در:
portal.example.com
Login کرده است.
Identity Provider نیز Session کاربر را دارد.
حالا کاربر وارد:
shop.example.com
میشود.
Shop کاربر را به:
auth.example.com/authorize
هدایت میکند.
Identity Provider Session کاربر را بررسی میکند.
اگر Session معتبر باشد، دیگر لازم نیست دوباره Username و Password درخواست شود.
در نتیجه Authorization Code جدید برای Shop صادر میشود.
فرآیند:
Portal
↓
Login at IdP
↓
IdP Session Created
↓
Portal Session Created
Shop
↓
Redirect to IdP
↓
Existing IdP Session
↓
Authorization Code
↓
Shop Session Created
این همان تجربهای است که کاربر آن را بهصورت SSO مشاهده میکند.
# ثبت Applicationها در Identity Provider
قبل از اینکه Applicationها بتوانند از IdP استفاده کنند، باید در آن ثبت شوند.
برای مثال Client مربوط به Portal:
client_id:
portal-web
Redirect URI:
https://portal.example.com/callback
Scope:
openid
profile
email
و برای Shop:
client_id:
shop-web
Redirect URI:
https://shop.example.com/callback
نکته مهم این است که Redirect URI باید دقیقاً بررسی شود.
نباید اجازه دهیم Client بتواند هر URL دلخواهی را به عنوان Redirect URI ارسال کند.
# ساختار دیتابیس Identity Provider
یک طراحی ساده میتواند شامل جدولهای زیر باشد:
users
oauth_clients
oauth_scopes
authorization_codes
access_tokens
refresh_tokens
user_identities
برای مثال جدول کاربران:
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255),
email_verified_at DATETIME NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
# جدول Clientها
مثلاً:
CREATE TABLE oauth_clients (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
client_id VARCHAR(100) NOT NULL UNIQUE,
client_secret_hash VARCHAR(255) NULL,
name VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL
);
در سیستم واقعی باید Redirect URIها، Scopeها و سایر تنظیمات Client نیز مدیریت شوند.
# مهمترین نکته در شناسایی کاربر
یکی از اشتباهات خطرناک این است که Applicationها را فقط با Email به یکدیگر متصل کنیم.
مثلاً:
email = user@example.com
بهعنوان شناسه اصلی هویت استفاده شود.
در OIDC بهتر است هویت خارجی کاربر بر اساس ترکیب:
issuer + subject
شناخته شود.
مثلاً:
iss = https://auth.example.com
sub = 123456
در نتیجه:
https://auth.example.com + 123456
هویت پایدار کاربر را تشکیل میدهد.
# جدول user_identities
میتوانیم چنین جدولی داشته باشیم:
CREATE TABLE user_identities (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
issuer VARCHAR(255) NOT NULL,
subject VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL,
UNIQUE KEY unique_identity (
issuer,
subject
)
);
این طراحی اجازه میدهد یک کاربر حتی با چند Identity Provider مختلف نیز مرتبط شود.
# پیادهسازی Login در PHP
فرض کنیم تنظیمات OIDC را در فایل زیر نگهداری کنیم:
config/oidc.php
نمونه:
<?php
return [
'issuer' => 'https://auth.example.com',
'client_id' => 'portal-web',
'client_secret' => $_ENV['OIDC_CLIENT_SECRET'],
'redirect_uri' => 'https://portal.example.com/callback',
'scopes' => [
'openid',
'profile',
'email',
],
];
Client Secret را نباید داخل Git یا کد عمومی قرار دهیم.
# ساخت State
قبل از Redirect کردن کاربر باید State تصادفی ایجاد کنیم.
<?php
session_start();
$state = bin2hex(random_bytes(32));
$_SESSION['oidc_state'] = $state;
state برای مقابله با حملات مربوط به درخواستهای جعلی OAuth استفاده میشود.
# ساخت Nonce
برای OIDC باید Nonce نیز داشته باشیم:
$nonce = bin2hex(random_bytes(32));
$_SESSION['oidc_nonce'] = $nonce;
سپس این مقدار را در Authorization Request قرار میدهیم.
بعد از دریافت ID Token باید Nonce موجود در Token را با مقدار Session مقایسه کنیم.
# استفاده از PKCE
برای جریان Authorization Code بهتر است PKCE نیز استفاده شود.
ابتدا:
$codeVerifier = rtrim(
strtr(
base64_encode(random_bytes(32)),
'+/',
'-_'
),
'='
);
سپس Code Challenge ساخته میشود.
$codeChallenge = rtrim(
strtr(
base64_encode(
hash('sha256', $codeVerifier, true)
),
'+/',
'-_'
),
'='
);
و در Session ذخیره میکنیم:
$_SESSION['oidc_code_verifier'] = $codeVerifier;
# ساخت Authorization URL
مثلاً:
$params = [
'client_id' => 'portal-web',
'redirect_uri' => 'https://portal.example.com/callback',
'response_type' => 'code',
'scope' => 'openid profile email',
'state' => $state,
'nonce' => $nonce,
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
];
$url = 'https://auth.example.com/authorize?' .
http_build_query($params);
header('Location: ' . $url);
exit;
در Application واقعی بهتر است Endpointها را از Discovery دریافت کنیم و URLها را Hard-code نکنیم.
# Callback
بعد از Login کاربر به Callback برمیگردد:
https://portal.example.com/callback
در PHP:
session_start();
if (!isset($_GET['code'], $_GET['state'])) {
http_response_code(400);
exit('Invalid callback.');
}
سپس State را بررسی میکنیم:
if (!hash_equals(
$_SESSION['oidc_state'] ?? '',
$_GET['state']
)) {
http_response_code(400);
exit('Invalid state.');
}
بعد از آن Authorization Code را دریافت میکنیم.
# تبدیل Authorization Code به Token
Application باید Code را به Token Endpoint ارسال کند.
نمونه ساده با cURL:
$ch = curl_init('https://auth.example.com/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'authorization_code',
'code' => $_GET['code'],
'redirect_uri' => 'https://portal.example.com/callback',
'client_id' => 'portal-web',
'client_secret' => $_ENV['OIDC_CLIENT_SECRET'],
'code_verifier' => $_SESSION['oidc_code_verifier'],
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
],
]);
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException(curl_error($ch));
}
curl_close($ch);
$tokens = json_decode($response, true);
در Production باید خطاها، HTTP Status، Timeout و اعتبار پاسخ نیز بهصورت کامل بررسی شوند.
# اعتبارسنجی ID Token
دریافت ID Token بهتنهایی به معنی معتبر بودن آن نیست.
باید حداقل موارد زیر بررسی شوند:
Signature
iss
aud
exp
iat
nonce
و بسته به معماری:
azp
auth_time
acr
amr
نیز ممکن است اهمیت داشته باشند.
برای بررسی Signature باید کلید عمومی Identity Provider از JWKS دریافت شود.
این موضوع مستقیماً به مقاله قبلی درباره:
JWT Key Rotation و JWKS در PHP
مرتبط است.
# بررسی Issuer
فرض کنیم Token دارای:
{
"iss": "https://auth.example.com"
}
باشد.
Application باید بررسی کند که Issuer دقیقاً همان مقدار مورد اعتماد آن است.
مثلاً:
$expectedIssuer = 'https://auth.example.com';
if (($claims['iss'] ?? null) !== $expectedIssuer) {
throw new RuntimeException('Invalid issuer.');
}
# بررسی Audience
اگر Client ما:
portal-web
است، باید بررسی کنیم که:
aud
در ID Token مربوط به همین Client باشد.
if (($claims['aud'] ?? null) !== 'portal-web') {
throw new RuntimeException('Invalid audience.');
}
در پیادهسازی واقعی باید حالتهایی که aud بهصورت آرایه ارائه میشود نیز مدیریت شوند.
# بررسی Expiration
Token دارای زمان انقضا است:
{
"exp": 1780003600
}
باید بررسی کنیم:
if (($claims['exp'] ?? 0) < time()) {
throw new RuntimeException('Token expired.');
}
نباید Token منقضیشده را معتبر در نظر بگیریم.
# بررسی Nonce
Nonce دریافتشده از ID Token باید با مقداری که قبل از Redirect ذخیره کرده بودیم برابر باشد.
if (!hash_equals(
$_SESSION['oidc_nonce'] ?? '',
$claims['nonce'] ?? ''
)) {
throw new RuntimeException('Invalid nonce.');
}
این بررسی یکی از قسمتهای مهم امنیت OIDC است.
# پیدا کردن کاربر در Application
بعد از اعتبارسنجی Token، Application باید کاربر را پیدا کند.
به جای:
SELECT * FROM users WHERE email = ?
بهتر است هویت OIDC را با:
issuer + subject
پیدا کنیم.
مثلاً:
$sql = "
SELECT user_id
FROM user_identities
WHERE issuer = ?
AND subject = ?
LIMIT 1
";
سپس Application User ID داخلی خودش را پیدا میکند.
# ایجاد Session محلی
بعد از اینکه کاربر با موفقیت احراز هویت شد، Application Session خودش را ایجاد میکند.
قبل از ایجاد Session جدید باید Session ID را Regenerate کنیم:
session_regenerate_id(true);
سپس:
$_SESSION['user_id'] = $userId;
$_SESSION['authenticated'] = true;
از اینجا به بعد Application دیگر برای هر صفحه مجبور نیست ID Token را دوباره بررسی کند.
Session محلی Application وظیفه Login داخلی را بر عهده میگیرد.
# Cookie امنیتی Session
Session Cookie باید تنظیمات امنیتی مناسب داشته باشد.
مثلاً:
session_set_cookie_params([
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
و تمام سیستم باید روی HTTPS اجرا شود.
# SSO بین چند Application
حالا فرض کنیم کاربر در Portal Login کرده است:
portal.example.com
↓
auth.example.com
↓
Login
↓
Portal Session
بعد کاربر وارد Shop میشود:
shop.example.com
↓
auth.example.com
↓
Existing IdP Session
↓
Authorization Code
↓
Shop Session
بنابراین:
Portal Session
≠
Shop Session
اما:
Portal
\
→ Identity Provider ← Shop
/
Admin
همه به یک هویت مرکزی اعتماد دارند.
# اتصال Application به API
فرض کنیم Shop میخواهد سفارشهای کاربر را از API دریافت کند.
درخواست:
GET https://api.example.com/orders
Authorization: Bearer ACCESS_TOKEN
API باید Access Token را اعتبارسنجی کند.
مثلاً:
Signature
Issuer
Audience
Expiration
Scopes
را بررسی کند.
# Scope چیست؟
فرض کنیم Application فقط اجازه مشاهده سفارشها را دارد.
Scope:
orders:read
باشد.
برای ایجاد سفارش:
orders:write
در نتیجه API میتواند بررسی کند:
Does this token have orders:read?
اگر نداشته باشد:
403 Forbidden
برگرداند.
# Authentication و Authorization را جدا کنیم
در این معماری:
Authentication:
این کاربر چه کسی است؟
Authorization:
این کاربر اجازه انجام چه کاری را دارد؟
OIDC بیشتر روی Authentication و انتقال Identity تمرکز دارد.
Scopeها، Roleها و Permissionها برای Authorization استفاده میشوند.
# Account Linking
گاهی یک کاربر ممکن است قبلاً در Application حساب داشته باشد.
مثلاً:
Local User ID: 25
Email: user@example.com
و بعد با OIDC وارد شود.
نباید صرفاً بر اساس Email بدون بررسیهای لازم حسابها را خودکار به هم متصل کنیم.
فرآیند Account Linking بهتر است کنترلشده باشد.
برای مثال:
User Login
↓
OIDC Identity
↓
issuer + subject
↓
Existing Identity?
/ \
Yes No
↓ ↓
Login Linking Flow
Email تأییدشده میتواند در فرآیند Linking کمککننده باشد، اما نباید جایگزین شناسه پایدار OIDC یعنی iss + sub شود.
# Logout در SSO
Logout در SSO کمی پیچیدهتر از Login است.
چون چند Session داریم:
Portal Session
Shop Session
Admin Session
IdP Session
اگر کاربر فقط از Portal خارج شود:
Portal Session → Destroyed
ممکن است Session مربوط به Identity Provider همچنان فعال باشد.
بنابراین اگر دوباره Login کنیم، ممکن است کاربر بدون وارد کردن Password به سیستم برگردد.
# RP-Initiated Logout
OpenID Connect مکانیزمی برای Logout نیز دارد.
Application میتواند کاربر را به Logout Endpoint مربوط به Identity Provider هدایت کند.
مثلاً:
https://auth.example.com/logout
و در صورت پشتیبانی Provider، پارامترهایی مانند:
id_token_hint
post_logout_redirect_uri
state
استفاده میشوند.
# چرا id_token_hint مهم است؟
Identity Provider با استفاده از id_token_hint میتواند متوجه شود که Logout مربوط به کدام Session یا کاربر است.
البته نحوه دقیق پشتیبانی به Provider و پیادهسازی OIDC بستگی دارد.
# Logout کامل در معماری چنداپلیکیشنی
یک معماری کاملتر ممکن است چنین باشد:
User
↓
Portal Logout
↓
IdP Logout
↓
IdP Session Destroyed
↓
Return to Portal
در معماریهای پیچیدهتر ممکن است Logout بین چند Application نیز هماهنگ شود.
OpenID Connect و اکوسیستم آن روشهایی مانند Front-Channel و Back-Channel Logout را نیز برای سناریوهای پیشرفتهتر ارائه میکنند.
# Discovery
Application نباید مجبور باشد تمام Endpointهای Identity Provider را بهصورت Hard-Code نگهداری کند.
OIDC معمولاً Discovery Endpoint دارد:
/.well-known/openid-configuration
مثلاً:
https://auth.example.com/.well-known/openid-configuration
این Endpoint اطلاعاتی مانند:
issuer
authorization_endpoint
token_endpoint
userinfo_endpoint
jwks_uri
end_session_endpoint
را ارائه میدهد.
در Application میتوانیم این اطلاعات را دریافت و Cache کنیم.
# چرا JWKS برای SSO اهمیت دارد؟
فرض کنید Identity Provider برای امضای ID Token از RSA استفاده میکند.
Application باید Public Key مربوطه را داشته باشد.
به جای قرار دادن Public Key داخل همه Applicationها، آنها میتوانند JWKS را از:
jwks_uri
دریافت کنند.
مثلاً:
https://auth.example.com/.well-known/jwks.json
و بر اساس:
kid
کلید صحیح را انتخاب کنند.
این همان معماریای است که در مقاله قبلی درباره JWT Key Rotation و JWKS بررسی کردیم.
# معماری پیشنهادی پروژه PHP
برای یک OIDC Client میتوانیم ساختار زیر را داشته باشیم:
project/
│
├── config/
│ └── oidc.php
│
├── src/
│ ├── OidcClient.php
│ ├── TokenValidator.php
│ ├── JwksClient.php
│ └── UserIdentityService.php
│
├── public/
│ ├── index.php
│ ├── login.php
│ ├── callback.php
│ └── logout.php
│
└── vendor/
# کلاس OidcClient
وظایف این کلاس میتواند شامل:
Discovery
Authorization URL
Token Exchange
UserInfo
Logout
باشد.
مثلاً:
class OidcClient
{
public function getAuthorizationUrl(): string
{
// Build authorization URL
}
public function exchangeCode(
string $code,
string $codeVerifier
): array {
// Exchange authorization code
}
public function getUserInfo(
string $accessToken
): array {
// Call UserInfo endpoint
}
}
# کلاس TokenValidator
این کلاس وظیفه بررسی Token را بر عهده دارد:
class TokenValidator
{
public function validate(
string $idToken
): array {
// Decode
// Find JWK by kid
// Verify signature
// Validate issuer
// Validate audience
// Validate expiration
// Validate nonce
return $claims;
}
}
این جداسازی باعث میشود منطق امنیتی در کل پروژه پخش نشود.
# نکته مهم درباره کتابخانهها
برای Production بهتر است برای رمزنگاری، JWT و OIDC از پیادهسازیهای تستشده استفاده کنیم و خودمان الگوریتمهای رمزنگاری را از صفر ننویسیم.
نوشتن دستی مواردی مثل:
RSA verification
JWT parser
JWK conversion
PKCE implementation
بدون شناخت کامل از استانداردها میتواند باعث ایجاد آسیبپذیری شود.
PHP میتواند بخش Application Logic را در اختیار ما قرار دهد و کتابخانههای استاندارد بخش Cryptographic Protocol را مدیریت کنند.
# چند Application با یک Identity Provider
حالا معماری کامل ما:
┌───────────────────────┐
│ Identity Provider │
│ │
│ auth.example.com │
└───────────┬───────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Portal │ │ Shop │ │ Admin │
│ │ │ │ │ │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└───────────────────────┼───────────────────────┘
▼
┌─────────────┐
│ API │
│ │
└─────────────┘
این معماری برای مجموعههایی که چند محصول یا Application دارند بسیار کاربردی است.
# یک سناریوی واقعی
فرض کنید یک شرکت سه محصول دارد:
crm.example.com
shop.example.com
support.example.com
و میخواهد همه آنها از یک حساب کاربری استفاده کنند.
Identity Provider:
auth.example.com
کاربر وارد CRM میشود:
Login
↓
auth.example.com
↓
CRM Session
سپس وارد Shop میشود:
Shop
↓
auth.example.com
↓
Existing Login
↓
Shop Session
بعد وارد Support میشود:
Support
↓
auth.example.com
↓
Existing Login
↓
Support Session
کاربر در تمام این سیستمها یک Identity مرکزی دارد.
# امنیت SSO
SSO به دلیل اینکه Identity Provider مرکزی است، باید با دقت بسیار زیادی پیادهسازی شود.
1. HTTPS
تمام ارتباطات باید روی HTTPS باشند.
https://auth.example.com
https://portal.example.com
https://shop.example.com
استفاده از HTTP برای Authentication قابل قبول نیست.
2. State
برای Authorization Request از:
state
استفاده کنید.
3. Nonce
برای OIDC از:
nonce
استفاده کنید و آن را در ID Token بررسی کنید.
4. PKCE
برای Authorization Code Flow از:
PKCE
استفاده کنید.
5. Redirect URI دقیق
این:
https://portal.example.com/callback
نباید بهصورت آزاد قابل تغییر باشد.
نباید اجازه دهیم مهاجم چیزی مانند:
https://evil.example.com/callback
را جایگزین کند.
6. بررسی Issuer
همیشه:
iss
را بررسی کنید.
7. بررسی Audience
همیشه مطمئن شوید Token برای Application خودتان صادر شده است.
8. بررسی Signature
Signature باید با Public Key مورد اعتماد Identity Provider بررسی شود.
9. بررسی Expiration
Token منقضیشده نباید معتبر در نظر گرفته شود.
10. محافظت از Session
بعد از Login:
session_regenerate_id(true);
را انجام دهید.
و Cookieها را با تنظیمات امنیتی مناسب ایجاد کنید.
11. Token را Log نکنید
مواردی مانند:
access_token
refresh_token
authorization_code
id_token
client_secret
نباید در Logهای معمول Application ذخیره شوند.
12. Key Rotation
Identity Provider باید امکان چرخش کلیدها را داشته باشد.
فرآیند مناسب:
Create New Key
↓
Publish JWKS
↓
Start Signing With New Key
↓
Old Key Retiring
↓
Wait For Old Tokens
↓
Remove Old Key
# اشتباهات رایج در SSO
اشتباه اول: اشتراک Session بین همه سایتها
این روش را بهعنوان جایگزین اصلی OIDC در معماری چند Application در نظر نگیرید.
اشتباه دوم: استفاده از Email بهعنوان شناسه اصلی
بهتر است Identity بر اساس:
issuer + subject
شناخته شود.
اشتباه سوم: حذف State
Authorization Request بدون State میتواند امنیت جریان OAuth را کاهش دهد.
اشتباه چهارم: حذف Nonce
در OIDC باید Nonce را جدی بگیرید.
اشتباه پنجم: عدم استفاده از PKCE
در Applicationهای جدید، PKCE باید بخشی از طراحی Authorization Code Flow باشد.
اشتباه ششم: اعتماد به Access Token بهعنوان Identity
Access Token برای Resource Server است.
ID Token برای انتقال اطلاعات Identity به Client استفاده میشود.
اشتباه هفتم: عدم بررسی Audience
ممکن است Token توسط Provider معتبر باشد اما برای Application شما صادر نشده باشد.
اشتباه هشتم: Redirect URI آزاد
Redirect URI باید از قبل ثبت و دقیقاً Validate شود.
اشتباه نهم: نگهداری Token در LocalStorage بدون بررسی معماری
نحوه نگهداری Token باید بر اساس نوع Application و مدل تهدید انتخاب شود.
برای Applicationهای Server-Side معمولاً Session امن سمت سرور انتخاب مناسبی برای نگهداری وضعیت Login است.
اشتباه دهم: نادیده گرفتن Key Rotation
اگر کلیدهای Identity Provider هیچوقت Rotate نشوند، مدیریت بلندمدت امنیت دشوارتر میشود.
# چکلیست پیادهسازی SSO در PHP
قبل از Production این موارد را بررسی کنید:
[ ] Identity Provider مرکزی
[ ] HTTPS
[ ] OIDC Discovery
[ ] Authorization Code Flow
[ ] PKCE
[ ] State
[ ] Nonce
[ ] Exact Redirect URI
[ ] ID Token Signature Validation
[ ] Issuer Validation
[ ] Audience Validation
[ ] Expiration Validation
[ ] JWKS
[ ] Key Rotation
[ ] Secure Session
[ ] session_regenerate_id()
[ ] Secure Cookie
[ ] HttpOnly Cookie
[ ] SameSite Cookie
[ ] Scope Management
[ ] API Authorization
[ ] Account Linking
[ ] Logout
[ ] Token Revocation/Expiration Strategy
[ ] Rate Limiting
[ ] Security Logging
[ ] عدم Log کردن Tokenها
# تفاوت معماری ساده و معماری SSO
معماری بدون SSO
Portal → Login System
Shop → Login System
Admin → Login System
هر Application مستقل است.
معماری SSO
Identity Provider
/ | \
/ | \
Portal Shop Admin
احراز هویت مرکزی میشود، اما Session هر Application میتواند مستقل باقی بماند.
# آیا SSO فقط برای سایتهاست؟
خیر.
SSO میتواند بین موارد مختلف استفاده شود:
Website
Admin Panel
Mobile App
Desktop App
Internal Tools
API
Microservices
Customer Portal
البته هر نوع Client نیازمندیهای امنیتی و معماری متفاوتی دارد.
# SSO و Laravel
اگرچه این مقاله بر پایه PHP نوشته شده، همین معماری را میتوان در Laravel نیز پیادهسازی کرد.
برای مثال:
Laravel Portal
↓
OpenID Connect
↓
Central Identity Provider
Laravel در اینجا مسئول Session و منطق Application است و Identity Provider مسئول احراز هویت مرکزی.
در معماریهای بزرگتر نیز میتوان از Role و Permissionهای داخلی Laravel برای Authorization استفاده کرد.
# SSO و WordPress
همین مدل را میتوان برای WordPress نیز استفاده کرد.
برای مثال:
auth.example.com
↓
WordPress
یا:
auth.example.com
↓
WordPress
↓
WooCommerce
در این حالت WordPress نقش OIDC Client را بازی میکند و Identity Provider مرکزی مدیریت Login را بر عهده دارد.
# جمعبندی
SSO یا Single Sign-On یکی از مهمترین الگوها برای مدیریت Authentication در معماریهای چنداپلیکیشنی است.
در معماری مبتنی بر OpenID Connect:
Identity Provider
↓
Authentication
↓
ID Token
↓
Application Session
و برای دسترسی به API:
Access Token
↓
Resource Server
↓
Authorization
این جداسازی باعث میشود:
- Authentication مرکزی شود
- Applicationها Session مستقل داشته باشند
- کاربران بتوانند با یک حساب وارد چند Application شوند
- مدیریت کاربران سادهتر شود
- Logout و Account Linking ساختارمندتر شود
- APIها بتوانند Access Token را مستقل از Session بررسی کنند
مهمترین نکته این است که SSO فقط به معنی «یک Username و Password برای چند سایت» نیست؛ بلکه یک معماری کامل برای مدیریت هویت و اعتماد بین چند Application است.
# سوالات متداول
آیا برای SSO باید Session همه سایتها مشترک باشد؟
خیر. در معماری OIDC هر Application میتواند Session خودش را داشته باشد و Identity Provider مسئول احراز هویت مرکزی باشد.
آیا Email برای شناسایی کاربر کافی است؟
بهتر است شناسه OIDC بر اساس ترکیب issuer + subject باشد. Email میتواند یک Attribute مهم باشد اما نباید جایگزین شناسه پایدار Identity شود.
تفاوت SSO و OAuth چیست؟
OAuth عمدتاً برای Authorization طراحی شده است؛ یعنی مشخص میکند یک Client به چه Resourceهایی دسترسی دارد. OpenID Connect یک لایه Identity روی OAuth 2.0 اضافه میکند و برای Authentication نیز استفاده میشود.
آیا Access Token همان ID Token است؟
خیر. ID Token اطلاعات هویت کاربر را برای Client منتقل میکند، در حالی که Access Token برای دسترسی به Resource Server استفاده میشود.
آیا میتوان SSO را بدون OpenID Connect ساخت؟
از نظر فنی روشهای دیگری نیز وجود دارند، اما برای Authentication مدرن بین چند Application، OpenID Connect یک استاندارد رایج و مناسب است.
اگر کلید JWT در Identity Provider تغییر کند چه اتفاقی میافتد؟
Application باید JWKS را بهدرستی مدیریت کند و بر اساس kid کلید جدید را پیدا کند. به همین دلیل Key Rotation و JWKS بخش مهمی از معماری SSO هستند.
آیا SSO امنیت را بیشتر میکند؟
SSO میتواند مدیریت هویت را متمرکز و استاندارد کند، اما Identity Provider به یک بخش بسیار حساس تبدیل میشود. بنابراین امنیت آن، مدیریت Session، Token، کلیدها و Endpointها اهمیت بسیار زیادی دارد.
# مقاله بعدی
بعد از پیادهسازی SSO، یکی از موضوعات مهم Logout یکپارچه بین چند Application است.
در مقاله بعدی میتوانیم بهصورت تخصصی بررسی کنیم:
پیادهسازی RP-Initiated Logout در OpenID Connect با PHP؛ مدیریت Logout بین چند اپلیکیشن
در آن مقاله، Login را ادامه نمیدهیم؛ بلکه روی چرخه کامل Logout، id_token_hint، post_logout_redirect_uri، Sessionهای محلی، Session مرکزی Identity Provider و سناریوهای چند Application تمرکز میکنیم.





