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

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

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

پیاده‌سازی SSO چنداپلیکیشنی با OpenID Connect در PHP؛ اتصال چند سایت به یک Identity Provider

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

مقدمه

فرض کنید یک مجموعه چند وب‌سایت و اپلیکیشن مختلف دارد:

  • auth.example.com
  • portal.example.com
  • shop.example.com
  • admin.example.com
  • api.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 تمرکز می‌کنیم.

نظرات (0)

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

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

مقالات مرتبط

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

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

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

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

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

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

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

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

۲۲ دقیقه