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

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

پیوندهای سریع
محصولات فیزیکیمحصولات دیجیتالدوره‌های آموزشی
سی ام اس نویس
مجله سی ام اس نویستماس با ماپنل کاربری
سی ام اس نویس — تمامی حقوق برای سی ام اس نویس محفوظ است. © 1406 تماس با ما09307711941
OAuth 2.0 Authorization Server در PHP؛ پیاده‌سازی عملی از صفر تا Production

OAuth 2.0 Authorization Server در PHP؛ پیاده‌سازی عملی از صفر تا Production

ممدیر کل سیستم ۳۰ شهریور ۱۴۰۵ ۲۳ دقیقه مطالعه ۲ بازدید
OAuth 2.0 Authorization Server در PHP؛ پیاده‌سازی عملی از صفر تا Production

مقدمه

در مقاله‌های قبلی یاد گرفتیم چگونه یک برنامه 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_challengePKCE
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 واقعی.

نظرات (0)

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

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

مقالات مرتبط

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

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

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

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

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

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

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

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

۲۲ دقیقه