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

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

پیوندهای سریع
محصولات فیزیکیمحصولات دیجیتالدوره‌های آموزشی
سی ام اس نویس
مجله سی ام اس نویستماس با ماپنل کاربری
سی ام اس نویس — تمامی حقوق برای سی ام اس نویس محفوظ است. © 1406 تماس با ما09307711941
ساخت OpenID Connect Provider در PHP؛ پیاده‌سازی Identity Provider از صفر تا Production

ساخت OpenID Connect Provider در PHP؛ پیاده‌سازی Identity Provider از صفر تا Production

ممدیر کل سیستم ۳۰ شهریور ۱۴۰۵ ۲۴ دقیقه مطالعه ۳ بازدید
ساخت OpenID Connect Provider در PHP؛ پیاده‌سازی Identity Provider از صفر تا Production

اگر در پروژه‌های 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 TokenUserInfo
هدفAuthenticationاطلاعات کاربر
نوع رایجJWTJSON
صادرکنندهProviderProvider
ارسال معمولToken ResponseAPI 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 محلی بسازد.

نظرات (0)

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

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

مقالات مرتبط

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

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

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

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

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

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

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

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

۲۲ دقیقه