مقدمه
در مقالههای قبلی یاد گرفتیم چگونه یک برنامه PHP میتواند از سرویسهایی مانند Google برای ورود کاربر استفاده کند.
در آن معماری، Google نقش یک Authorization Server را بر عهده داشت و برنامه PHP ما یک Client بود.
اما اگر بخواهیم برعکس عمل کنیم چه؟
فرض کنید یک شرکت چند سرویس مختلف دارد:
example.com
api.example.com
admin.example.com
mobile.example.com
و میخواهد کاربران فقط یک بار احراز هویت شوند و برنامههای مختلف بتوانند با اجازه کاربر به APIهای مشخصی دسترسی پیدا کنند.
در این حالت میتوانیم یک OAuth 2.0 Authorization Server داشته باشیم:
┌─────────────────────┐
│ Authorization Server│
│ PHP │
└──────────┬──────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Web Client Mobile Client Admin
│ │ │
└──────────────┼──────────────┘
▼
Resource API
در این مقاله یک معماری عملی برای ساخت چنین سیستمی در PHP را بررسی میکنیم.
> نکته مهم: پیادهسازی کامل OAuth 2.0 یک پروژه امنیتی حساس است. کدهای این مقاله برای آموزش معماری و مفاهیم طراحی شدهاند و برای Production باید با کتابخانهها و استانداردهای معتبر OAuth 2.0 تطبیق داده شوند.
# 1. Authorization Server چیست؟
Authorization Server سرویسی است که مسئول صدور و مدیریت Tokenهای OAuth است.
این Server معمولاً کارهایی مانند:
Authentication
Consent
Authorization
Authorization Code
Access Token
Refresh Token
Scope
Client Validation
Token Revocation
را مدیریت میکند.
در معماری ساده:
User
↓
Authorization Server
↓
Authorization Code
↓
Client
↓
Access Token
↓
Resource Server
# 2. چهار نقش اصلی OAuth 2.0
در OAuth 2.0 چهار نقش اصلی داریم.
Resource Owner
معمولاً کاربر است.
مثلاً:
Ali
که صاحب اطلاعات حساب خودش است.
Client
برنامهای که میخواهد به Resource دسترسی پیدا کند.
مثلاً:
web-app
mobile-app
desktop-app
Authorization Server
سرویسی که Authorization را مدیریت و Token صادر میکند.
در پروژه ما:
auth.example.com
است.
Resource Server
سرویسی که API یا داده واقعی در آن قرار دارد.
مثلاً:
api.example.com
# 3. معماری کلی
فرض کنیم سه سرویس داریم:
auth.example.com
api.example.com
app.example.com
جریان:
┌──────────────────┐
│ User │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Authorization │
│ Server │
│ auth.example.com │
└────────┬─────────┘
│
Access Token
│
▼
┌──────────────────┐
│ Resource Server │
│ api.example.com │
└──────────────────┘
# 4. چرا Authorization Server جدا داشته باشیم؟
در پروژههای کوچک ممکن است Authentication و API داخل یک Application باشند.
اما در معماریهای بزرگ، جدا کردن Identity و Authorization میتواند مزایایی داشته باشد:
Authentication
│
▼
Authorization Server
│
├── Web
├── Mobile
├── Admin
└── Third-party Apps
در نتیجه Clientهای مختلف میتوانند از یک سیستم Authorization استفاده کنند.
# 5. OAuth 2.0 Flow مورد استفاده
در این مقاله از:
Authorization Code Flow
استفاده میکنیم.
جریان:
User
↓
Client
↓
Authorization Server
↓
Login
↓
Consent
↓
Authorization Code
↓
Client
↓
Token Endpoint
↓
Access Token
برای Clientهای عمومی مانند SPA یا Mobile، استفاده از PKCE اهمیت ویژهای دارد.
# 6. چرا Authorization Code Flow؟
یکی از دلایل اصلی این است که Credentialهای حساس Server-side میتوانند در سمت Server باقی بمانند.
در این Flow:
User Password
مستقیماً در اختیار Client قرار نمیگیرد.
Client فقط نتیجه Authorization را دریافت میکند.
# 7. ساختار پروژه PHP
برای پروژه آموزشی میتوانیم چنین ساختاری داشته باشیم:
oauth-server/
│
├── config/
│ └── oauth.php
│
├── public/
│ ├── authorize.php
│ ├── token.php
│ └── revoke.php
│
├── src/
│ ├── OAuth/
│ │ ├── ClientService.php
│ │ ├── AuthorizationService.php
│ │ ├── TokenService.php
│ │ └── ScopeService.php
│ │
│ └── Database/
│
└── vendor/
# 8. جدول Clients
Authorization Server باید Clientها را بشناسد.
مثلاً:
CREATE TABLE oauth_clients (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
client_id VARCHAR(100) NOT NULL UNIQUE,
client_secret_hash CHAR(64) NULL,
name VARCHAR(150) NOT NULL,
redirect_uris TEXT NOT NULL,
client_type VARCHAR(30) NOT NULL,
revoked_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Client Type میتواند مثلاً:
confidential
public
باشد.
# 9. Confidential و Public Client
Confidential Client
Clientای که میتواند Secret را به شکل امن نگهداری کند.
مثلاً:
Backend Application
Public Client
Clientای که نمیتواند Secret را به شکل امن نگهداری کند.
مثلاً:
Mobile App
SPA
Desktop App
برای Public Clientها استفاده از PKCE بسیار مهم است.
# 10. Client ID
برای هر Client یک شناسه ایجاد میکنیم:
client_123456
یا یک مقدار تصادفی:
$clientId = bin2hex(
random_bytes(16)
);
# 11. Client Secret
برای Confidential Client میتوان Secret تولید کرد:
$clientSecret = bin2hex(
random_bytes(32)
);
اما بهتر است Raw Secret را در دیتابیس ذخیره نکنیم.
مثلاً:
$secretHash = hash(
'sha256',
$clientSecret
);
و Hash را ذخیره کنیم.
# 12. Redirect URI
هر Client باید Redirect URI مشخص داشته باشد.
مثلاً:
https://app.example.com/oauth/callback
Authorization Server نباید هر Redirect URI دلخواهی را قبول کند.
این کار میتواند زمینه حملات جدی ایجاد کند.
# 13. Authorization Request
Client کاربر را به Endpoint زیر میفرستد:
GET /oauth/authorize
مثلاً:
https://auth.example.com/oauth/authorize?
response_type=code
&client_id=client_123
&redirect_uri=https://app.example.com/callback
&scope=profile email
&state=abc123
&code_challenge=...
&code_challenge_method=S256
# 14. پارامترهای مهم Authorization Request
| پارامتر | کاربرد |
|---|---|
response_type | نوع Response |
client_id | شناسایی Client |
redirect_uri | مقصد Callback |
scope | سطح دسترسی |
state | محافظت جریان OAuth |
code_challenge | PKCE |
code_challenge_method | الگوریتم PKCE |
# 15. بررسی Client
وقتی درخواست دریافت شد، ابتدا Client را پیدا میکنیم:
SELECT *
FROM oauth_clients
WHERE client_id = ?
AND revoked_at IS NULL
LIMIT 1;
اگر Client پیدا نشد:
400 Bad Request
برمیگردانیم.
# 16. بررسی Redirect URI
یکی از مهمترین مراحل:
Requested Redirect URI
↓
Registered Redirect URI
باید دقیقاً با یکی از URIهای ثبتشده Client مطابقت داشته باشد.
نباید کاری مانند این انجام دهیم:
if (str_starts_with(
$redirectUri,
'https://app.example.com'
)) {
// accept
}
چنین بررسیهایی میتواند در برابر Redirect URIهای مخرب مشکلساز شود.
بهتر است URI دقیقاً از لیست URIهای مجاز انتخاب شود.
# 17. بررسی Response Type
اگر فقط Authorization Code را پشتیبانی میکنیم:
if ($_GET['response_type'] !== 'code') {
http_response_code(400);
exit('Unsupported response type.');
}
# 18. احراز هویت کاربر
Authorization Server باید بداند چه کاربری در حال Authorization است.
مثلاً:
Session
↓
Authenticated User
اگر کاربر Login نکرده باشد:
/oauth/authorize
↓
Login
↓
Return to Authorization
# 19. Consent Screen
بعد از Login میتوانیم صفحه Consent نمایش دهیم.
مثلاً:
اپلیکیشن Example درخواست دسترسی دارد:
☑ مشاهده اطلاعات پروفایل
☑ مشاهده Email
[اجازه میدهم]
[لغو]
این مرحله برای OAuth بسیار مهم است.
کاربر باید بداند Client چه Scopeهایی درخواست کرده است.
# 20. Scope چیست؟
Scope سطح دسترسی را مشخص میکند.
مثلاً:
profile
email
orders.read
orders.write
مثلاً:
orders.read
یعنی Client فقط اجازه خواندن سفارشها را دارد.
اما:
orders.write
میتواند اجازه تغییر سفارشها را بدهد.
# 21. اصل Least Privilege
Client نباید بیشتر از نیاز خود Scope دریافت کند.
مثلاً اگر یک Application فقط Email را نیاز دارد:
email
کافی است.
نباید بدون نیاز:
profile
orders.read
orders.write
admin
را درخواست کنیم.
# 22. جدول Scopeها
میتوانیم Scopeها را در دیتابیس مدیریت کنیم:
CREATE TABLE oauth_scopes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
description VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
مثلاً:
profile
email
orders.read
orders.write
# 23. Authorization Code چیست؟
بعد از اینکه:
Client
+
User
+
Redirect URI
+
Scope
+
Consent
تأیید شدند، Authorization Server یک Code ایجاد میکند.
مثلاً:
4e7d9f...
این Code نباید Token دائمی باشد.
# 24. تولید Authorization Code
از مقدار تصادفی امن استفاده میکنیم:
$code = bin2hex(
random_bytes(32)
);
اما بهتر است Raw Code را در دیتابیس ذخیره نکنیم.
Hash:
$codeHash = hash(
'sha256',
$code
);
# 25. جدول Authorization Code
CREATE TABLE oauth_authorization_codes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
code_hash CHAR(64) NOT NULL UNIQUE,
client_id VARCHAR(100) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
redirect_uri TEXT NOT NULL,
scope TEXT NOT 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 DEFAULT CURRENT_TIMESTAMP
);
# 26. چرا Authorization Code باید کوتاهعمر باشد؟
Authorization Code فقط یک مرحله موقت بین:
Authorization Endpoint
↓
Token Endpoint
است.
بنابراین باید عمر کوتاهی داشته باشد.
مثلاً چند دقیقه.
همچنین باید:
One-Time Use
باشد.
# 27. Redirect کردن Code
بعد از Authorization موفق:
$location =
$redirectUri
. '?'
. http_build_query([
'code' => $code,
'state' => $state,
]);
header(
'Location: ' . $location
);
exit;
Client سپس Code را دریافت میکند.
# 28. چرا State را Client مدیریت میکند؟
Client باید State اولیه را تولید و ذخیره کرده باشد.
Authorization Server آن را بدون تغییر برمیگرداند.
مثلاً:
Client
state = ABC123
↓
Authorization Server
↓
callback
state = ABC123
Client باید بررسی کند:
Received State == Stored State
# 29. Token Endpoint
حالا Client باید Code را به Token Endpoint بفرستد:
POST /oauth/token
مثلاً:
grant_type=authorization_code
code=...
client_id=...
redirect_uri=...
code_verifier=...
# 30. بررسی Authorization Code
Server ابتدا Code را Hash میکند:
$codeHash = hash(
'sha256',
$code
);
سپس:
SELECT *
FROM oauth_authorization_codes
WHERE code_hash = ?
LIMIT 1;
# 31. بررسی Expiration
if (
strtotime($record['expires_at'])
<= time()
) {
http_response_code(400);
exit('Authorization code expired.');
}
# 32. بررسی استفاده قبلی
اگر:
used_at != NULL
باشد، Code قبلاً استفاده شده است.
باید رد شود:
if ($record['used_at'] !== null) {
http_response_code(400);
exit('Authorization code already used.');
}
# 33. بررسی Client
Client درخواست Token باید همان Client باشد که Code برای آن صادر شده است.
مثلاً:
if ($record['client_id'] !== $clientId) {
http_response_code(400);
exit('Invalid client.');
}
# 34. بررسی Redirect URI
Redirect URI نیز باید با مقداری که هنگام Authorization استفاده شده یکسان باشد.
if (
$record['redirect_uri']
!== $redirectUri
) {
http_response_code(400);
exit('Invalid redirect URI.');
}
# 35. بررسی PKCE
اگر Code هنگام Authorization با PKCE ایجاد شده باشد، Client باید:
code_verifier
را ارسال کند.
Server دوباره Code Challenge را محاسبه میکند.
برای S256:
BASE64URL(
SHA256(code_verifier)
)
و باید با code_challenge ذخیرهشده مطابقت داشته باشد.
# 36. پیادهسازی PKCE
مثلاً:
$challenge = rtrim(
strtr(
base64_encode(
hash(
'sha256',
$codeVerifier,
true
)
),
'+/',
'-_'
),
'='
);
سپس:
if (
!hash_equals(
$record['code_challenge'],
$challenge
)
) {
http_response_code(400);
exit('Invalid PKCE verification.');
}
# 37. تبدیل Authorization Code به Token
اگر همه بررسیها موفق باشند:
Authorization Code
↓
Validation
↓
PKCE
↓
Client Validation
↓
Token Issuance
اکنون میتوانیم Access Token صادر کنیم.
# 38. Access Token تصادفی
یک روش ساده برای Access Token:
$accessToken = bin2hex(
random_bytes(32)
);
Hash:
$accessTokenHash = hash(
'sha256',
$accessToken
);
# 39. جدول Access Token
CREATE TABLE oauth_access_tokens (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
token_hash CHAR(64) NOT NULL UNIQUE,
client_id VARCHAR(100) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
scope TEXT NOT NULL,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_access_user (user_id),
INDEX idx_access_client (client_id)
);
# 40. Refresh Token
در کنار Access Token میتوان Refresh Token نیز ایجاد کرد:
$refreshToken = bin2hex(
random_bytes(32)
);
$refreshTokenHash = hash(
'sha256',
$refreshToken
);
سپس در جدول Refresh Token ذخیره میکنیم.
# 41. پاسخ Token Endpoint
در صورت موفقیت:
{
"token_type": "Bearer",
"access_token": "ACCESS_TOKEN",
"expires_in": 900,
"refresh_token": "REFRESH_TOKEN",
"scope": "profile email"
}
# 42. چرا Access Token و Refresh Token جدا هستند؟
چون کاربرد آنها متفاوت است.
Access Token
↓
API Access
در مقابل:
Refresh Token
↓
New Access Token
به همین دلیل میتوانیم Access Token را کوتاهعمر و Refresh Token را بلندعمرتر طراحی کنیم.
# 43. Resource Server
حالا api.example.com باید Access Token را بررسی کند.
Client:
GET /api/profile
Authorization: Bearer ACCESS_TOKEN
Resource Server Token را دریافت میکند.
# 44. بررسی Access Token
اگر Access Token Opaque باشد:
$tokenHash = hash(
'sha256',
$accessToken
);
سپس:
SELECT *
FROM oauth_access_tokens
WHERE token_hash = ?
AND revoked_at IS NULL
AND expires_at > NOW()
LIMIT 1;
اگر نتیجه وجود داشته باشد:
Token Valid
# 45. بررسی Scope در Resource Server
فرض کنیم Endpoint:
GET /api/orders
به Scope:
orders.read
نیاز دارد.
بعد از اعتبارسنجی Token:
$scopes = explode(
' ',
$tokenRecord['scope']
);
سپس:
if (!in_array(
'orders.read',
$scopes,
true
)) {
http_response_code(403);
exit('Insufficient scope.');
}
این تفاوت مهم است:
401
→ Authentication / Token مشکل دارد
403
→ Token معتبر است اما Permission کافی نیست
# 46. Authorization و Authentication
این دو را باید جدا نگه داریم.
Authentication
Who are you?
Authorization
What are you allowed to do?
مثلاً:
User = 25
احراز هویت است.
اما:
User 25
Scope = orders.read
مجوز دسترسی است.
# 47. Token Revocation
باید Endpointی برای Revocation داشته باشیم:
POST /oauth/revoke
مثلاً Client Token را ارسال میکند.
Server آن را پیدا میکند و:
UPDATE oauth_access_tokens
SET revoked_at = NOW()
WHERE token_hash = ?;
انجام میدهد.
برای Refresh Token نیز میتوان همین مفهوم را اعمال کرد.
# 48. Refresh Token Endpoint
Client میتواند:
POST /oauth/token
را با:
grant_type=refresh_token
صدا بزند.
مثلاً:
grant_type=refresh_token
refresh_token=...
client_id=...
Server:
Validate Refresh Token
↓
Check Expiration
↓
Check Revocation
↓
Rotate
↓
New Access Token
↓
New Refresh Token
را اجرا میکند.
# 49. Token Rotation
اگر Refresh Token قبلی:
RT-A
باشد، بعد از استفاده:
RT-A → Revoked
RT-B → New
تولید میشود.
اگر RT-A دوباره استفاده شود:
Reuse Detected
و میتوان Token Family مربوطه را Revoked کرد.
# 50. Token Family
در سیستمهای حرفهای میتوانیم:
family_id
داشته باشیم.
مثلاً:
Family 123
RT-A
↓
RT-B
↓
RT-C
↓
RT-D
این ساختار برای تشخیص Reuse بسیار مفید است.
# 51. Consent و Authorization
Consent را نباید صرفاً یک صفحه ظاهری در نظر گرفت.
Authorization Server باید بداند:
User
Client
Requested Scope
Approved Scope
Time
چیست.
برای مثال میتوان جدول Consent ایجاد کرد:
CREATE TABLE oauth_consents (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
client_id VARCHAR(100) NOT NULL,
scopes TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
# 52. آیا هر بار Consent نمایش داده شود؟
لزومی ندارد.
میتوانیم سیاستی مانند:
User + Client + Scope
داشته باشیم.
اگر قبلاً اجازه داده شده باشد و Scope جدیدی درخواست نشده باشد، میتوان در معماری مناسب از نمایش مجدد Consent صرفنظر کرد.
اما این تصمیم باید با سیاست امنیتی و تجربه کاربری سیستم هماهنگ باشد.
# 53. Client Authentication
برای Confidential Client میتوان Secret دریافت کرد.
مثلاً:
client_id
client_secret
اما Public Client نباید به داشتن Secret واقعی وابسته باشد.
در چنین مواردی PKCE اهمیت ویژهای دارد.
# 54. Secret را چگونه بررسی کنیم؟
اگر Hash Secret ذخیره شده باشد:
$secretHash = hash(
'sha256',
$clientSecret
);
و سپس:
hash_equals(
$storedHash,
$secretHash
);
بررسی میکنیم.
# 55. چرا Client Secret را Plaintext ذخیره نکنیم؟
فرض کنید Database لو برود.
اگر:
client_secret = raw-secret
ذخیره شده باشد، مهاجم Secret را مستقیماً در اختیار دارد.
بهتر است Secretهای قابل Hash شدن را با طراحی مناسب به صورت Hash نگهداری کنیم.
# 56. State و PKCE
این دو مفهوم را نباید با هم اشتباه بگیریم.
State
بیشتر برای ارتباط درخواست و Callback و جلوگیری از CSRF در Client استفاده میشود.
PKCE
Authorization Code را به Clientی که Code Challenge را ایجاد کرده، Bind میکند.
هر دو میتوانند در یک Flow حضور داشته باشند.
# 57. Authorization Server نباید Password Client را ببیند؟
Client Password و User Password دو مفهوم متفاوت هستند.
User:
Username + Password
برای Login کاربر.
Client:
Client ID + Client Secret
برای احراز هویت Confidential Client.
اینها نباید با هم اشتباه شوند.
# 58. Security Headers
Authorization Server باید مانند هر Application حساس Web، Security Headerهای مناسب داشته باشد.
مثلاً:
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
و در معماری مناسب:
Content-Security-Policy
نیز تنظیم شود.
# 59. Session Security
چون Authorization Server احتمالاً Login دارد، Session Security اهمیت بالایی دارد.
بعد از Login:
session_regenerate_id(true);
و Cookieهای Session باید ویژگیهایی مانند:
Secure
HttpOnly
SameSite
داشته باشند.
# 60. HTTPS
Authorization Server باید در Production روی HTTPS اجرا شود.
اطلاعات حساسی مانند:
Authorization Code
Client Secret
Access Token
Refresh Token
Session Cookie
نباید از طریق HTTP معمولی منتقل شوند.
# 61. Rate Limiting
Endpointهای حساس باید Rate Limit داشته باشند:
/oauth/authorize
/oauth/token
/oauth/revoke
/login
بهخصوص:
Login
Token
Refresh
نیازمند کنترل Abuse هستند.
# 62. ثبت رویدادهای امنیتی
میتوان Eventهایی مانند اینها را ثبت کرد:
oauth_authorization_started
oauth_authorization_denied
oauth_code_issued
oauth_token_issued
oauth_refresh
oauth_token_revoked
oauth_refresh_reuse_detected
اما نباید Token خام یا Secret را داخل Log ذخیره کنیم.
# 63. مدیریت Error
OAuth Errorها باید کنترلشده باشند.
مثلاً:
invalid_request
invalid_client
invalid_grant
unauthorized_client
unsupported_grant_type
invalid_scope
access_denied
مثلاً:
{
"error": "invalid_grant"
}
نباید اطلاعات حساس Database یا Stack Trace را به Client نمایش دهیم.
# 64. ساختار Service Layer
برای پروژه Production بهتر است منطق را جدا کنیم:
OAuth/
├── ClientService.php
├── ScopeService.php
├── AuthorizationService.php
├── AuthorizationCodeService.php
├── AccessTokenService.php
├── RefreshTokenService.php
└── ConsentService.php
Controller فقط Request را دریافت کند و Service مناسب را صدا بزند.
# 65. نمونه AuthorizationService
ساختار مفهومی:
class AuthorizationService
{
public function authorize(
int $userId,
string $clientId,
array $scopes,
string $redirectUri
) {
// Validate client
// Validate redirect URI
// Validate scopes
// Create authorization code
// Return redirect
}
}
# 66. نمونه TokenService
class TokenService
{
public function issueAccessToken(
int $userId,
string $clientId,
array $scopes
) {
// Generate token
// Store hash
// Set expiration
// Return raw token
}
}
# 67. نمونه RefreshTokenService
class RefreshTokenService
{
public function rotate(
string $refreshToken
) {
// Find token
// Lock record
// Validate
// Revoke old token
// Create new token
// Return new tokens
}
}
این جداسازی باعث میشود تست کردن و توسعه سیستم بسیار سادهتر شود.
# 68. Transaction در Token Issuance
صدور Token میتواند چند عملیات داشته باشد:
Create Access Token
Create Refresh Token
Mark Code Used
Create Session/Record
در صورت نیاز باید این عملیات داخل Transaction انجام شوند:
$pdo->beginTransaction();
try {
// Mark code as used
// Create access token
// Create refresh token
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
# 69. چرا Authorization Code باید اتمیک مصرف شود؟
فرض کنید دو Request همزمان Code یکسانی ارسال کنند:
Request A ──┐
├── Code XYZ
Request B ──┘
اگر هر دو Request قبل از Mark شدن Code به آن دسترسی پیدا کنند، ممکن است Code دوبار مصرف شود.
بنابراین باید از:
Transaction
+
Row Lock
+
One-Time Use
استفاده کنیم.
# 70. معماری نهایی
در نهایت معماری سیستم تقریباً چنین خواهد بود:
USER
│
▼
┌─────────────────┐
│ Authorization │
│ Server │
└────────┬────────┘
│
Login + Consent
│
▼
Authorization Code
│
▼
CLIENT
│
▼
Token Endpoint
│
┌────────┴────────┐
│ │
▼ ▼
Access Token Refresh Token
│ │
▼ ▼
Resource Server Token Endpoint
│ │
▼ ▼
API Data New Tokens
# 71. چکلیست Production
قبل از استفاده واقعی از Authorization Server این موارد را بررسی کنید:
☑ HTTPS فعال است
☑ Redirect URI دقیقاً بررسی میشود
☑ Client ID اعتبارسنجی میشود
☑ Client Secret امن نگهداری میشود
☑ Public و Confidential Client تفکیک شدهاند
☑ Authorization Code کوتاهعمر است
☑ Authorization Code یکبارمصرف است
☑ PKCE برای Flowهای مناسب فعال است
☑ State در Client استفاده میشود
☑ Scopeها Allowlist هستند
☑ Consent مدیریت میشود
☑ Access Token Expiration دارد
☑ Refresh Token Expiration دارد
☑ Refresh Token Rotation فعال است
☑ Refresh Token Revocation دارد
☑ Reuse Detection در نظر گرفته شده
☑ Tokenها در Log ذخیره نمیشوند
☑ Rate Limiting فعال است
☑ Session Security رعایت شده
☑ Security Headers تنظیم شدهاند
☑ Errorها اطلاعات حساس افشا نمیکنند
☑ Database Transaction استفاده شده
☑ Race Condition بررسی شده
☑ Tokenهای قدیمی Cleanup میشوند
# 72. چه زمانی نباید Authorization Server خودمان را بسازیم؟
این بخش بسیار مهم است.
اگر پروژه شما فقط نیاز دارد:
Login with Google
Login with GitHub
API Authentication
لزومی ندارد حتماً Authorization Server اختصاصی بسازید.
ساخت یک Authorization Server واقعی پروژه پیچیدهای است و باید استانداردهای OAuth 2.0، امنیت Token، Consent، Client Registration، Revocation و موارد دیگر را بهدرستی پوشش دهد.
برای پروژههای Production حساس، استفاده از یک راهکار OAuth/OIDC معتبر و تستشده میتواند نسبت به پیادهسازی کامل پروتکل از صفر مناسبتر باشد.
# 73. تفاوت API Authentication ساده با OAuth Server
برای یک API داخلی ممکن است این کافی باشد:
API Key
یا:
Bearer Token
اما OAuth Server زمانی ارزش بیشتری پیدا میکند که چند Client و چند Resource داشته باشیم:
OAuth Server
/ | \
/ | \
Web Mobile Partner
\ | /
\ | /
API Resources
# 74. OAuth Server و OpenID Connect
اگر علاوه بر Authorization بخواهیم Identity Provider نیز باشیم، میتوانیم OpenID Connect را روی OAuth 2.0 اضافه کنیم.
در این حالت علاوه بر:
Access Token
Refresh Token
ممکن است:
ID Token
نیز صادر کنیم.
این همان مدلی است که در Login با Google بررسی کردیم.
# 75. مسیر توسعه سیستم
اگر بخواهیم این پروژه را مرحلهبهمرحله توسعه دهیم، مسیر منطقی میتواند این باشد:
مرحله 1
OAuth Client
مرحله 2
Authorization Endpoint
مرحله 3
Authorization Code
مرحله 4
PKCE
مرحله 5
Token Endpoint
مرحله 6
Access Token
مرحله 7
Refresh Token
مرحله 8
Token Rotation
مرحله 9
Revocation
مرحله 10
Scope
مرحله 11
Consent
مرحله 12
OpenID Connect
مرحله 13
Discovery
مرحله 14
JWKS
مرحله 15
Production Hardening
این مسیر به شما کمک میکند OAuth را فقط به شکل تئوری یاد نگیرید و معماری کامل آن را درک کنید.
# پرسشهای متداول
Authorization Server چیست؟
سرویسی است که Authorization را مدیریت میکند و در OAuth 2.0 میتواند Authorization Code، Access Token و Refresh Token صادر کند.
Resource Server چیست؟
سرویسی است که Resource واقعی را ارائه میکند و Access Token را برای دسترسی به API بررسی میکند.
آیا Authorization Server همان API Server است؟
لزومی ندارد. در معماریهای کوچک ممکن است در یک Application باشند، اما در معماریهای بزرگ میتوانند کاملاً جدا باشند.
آیا برای هر پروژه PHP باید OAuth Server بسازیم؟
خیر. اگر فقط Authentication ساده یا Login با Google نیاز دارید، ساخت Authorization Server اختصاصی ممکن است پیچیدگی غیرضروری ایجاد کند.
آیا PKCE فقط برای Mobile است؟
خیر. PKCE در بسیاری از Authorization Code Flowها میتواند یک لایه امنیتی مهم باشد و برای Public Clientها اهمیت ویژهای دارد.
آیا Access Token باید JWT باشد؟
خیر. Access Token میتواند Opaque Token نیز باشد. انتخاب بین JWT و Opaque Token به معماری سیستم بستگی دارد.
آیا Refresh Token باید JWT باشد؟
خیر. یک Token تصادفی و Opaque میتواند گزینه مناسبی باشد و مدیریت Revocation و Rotation را سادهتر کند.
# جمعبندی
در این مقاله از مصرف OAuth 2.0 عبور کردیم و معماری یک Authorization Server در PHP را بررسی کردیم.
ساختار اصلی چنین بود:
User
↓
Authorization Server
↓
Login
↓
Consent
↓
Authorization Code
↓
PKCE Verification
↓
Token Endpoint
↓
Access Token + Refresh Token
↓
Resource Server
همچنین دیدیم که یک Authorization Server واقعی فقط یک Endpoint برای صدور Token نیست.
بخشهایی مانند:
Client Management
Redirect URI Validation
Scope
Consent
Authorization Code
PKCE
Access Token
Refresh Token
Rotation
Revocation
Reuse Detection
Rate Limiting
Session Security
همگی بخشی از معماری آن هستند.
مهمتر از همه، Authorization Server یکی از قسمتهایی است که پیادهسازی امنیتی آن نباید با یک نمونه ساده آموزشی با Production اشتباه گرفته شود. در سیستمهای واقعی، استفاده از پیادهسازیها و کتابخانههای OAuth/OIDC معتبر و تستشده میتواند بخش قابلتوجهی از ریسک خطاهای امنیتی را کاهش دهد.
مقاله بعدی
بعد از ساخت OAuth 2.0 Authorization Server، قدم منطقی بعدی تکمیل آن با OpenID Connect Provider در PHP است؛ یعنی ساخت ID Token، پیادهسازی /.well-known/openid-configuration، JWKS Endpoint، مدیریت nonce و Claimهای استاندارد و در نهایت تبدیل Authorization Server به یک Identity Provider واقعی.





