> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# SAML للمبتدئين: متى ما زالت المؤسسات تحتاجه؟
- URL: https://arabroot.io/articles/شرح-saml-للمبتدئين/
- Published: 2026-10-25T19:11:24.000Z
- Updated: 2026-10-26T05:53:49.000Z
- Description: نشرح SAML بتجربة حقيقية نرى فيها الـ Assertion كما يحمله المتصفح، ثم نعدله ونسرقه ونؤخره لنرى كيف يرفضه التطبيق.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, الاستضافة الذاتية, SimpleSAMLphp

تشترك مؤسستك في خدمة سحابية جديدة، وفي صفحة إعداد الدخول الموحد تجد خياراً واحداً هو SAML، ويطلب منك المورد «ملف Metadata الخاص بمزود الهوية»، ويعطيك هو «Entity ID» و«ACS URL». وكما ذكرنا في [شرح الدخول الموحد SSO ومصطلحاته](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%88%D8%AD%D8%AF-sso-%D9%88%D8%A7%D9%84%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA/) فإن الدخول الموحد عبر الويب له لغتان اليوم: OpenID Connect الأحدث، وSAML الأقدم الذي ما زال يحكم جزءاً كبيراً من تطبيقات المؤسسات. وفي هذا المقال سوف نفهم SAML من الداخل، بتجربة حقيقية نرى فيها الرسالة التي يحملها المتصفح كما هي، ثم نعبث بها ونرى كيف يرفضها التطبيق.

وسوف نناقش في هذا المقال ما يلي:

- لماذا ما زالت المؤسسات تحتاج SAML في 2026، ومتى تختاره ومتى تختار OpenID Connect.
- المصطلحات التي تظهر في كل شاشة إعداد: Entity ID وMetadata وACS والـ Bindings وNameID.
- خطوات الدخول بالترتيب، من الطلب AuthnRequest إلى الرد SAMLResponse.
- تجربة عملية بمزود هوية صغير وتطبيق تجريبي على SimpleSAMLphp في Docker، مع الـ Assertion الحقيقي وتشريحه.
- أربعة أعطال نجربها عن قصد: رسالة معدلة، ورسالة مسروقة، ورسالة منتهية، ورسالة مكررة.
- ثغرات SAML المعروفة، وقائمة تحقق قبل أن تربط تطبيقاً حقيقياً.

## لماذا ما زالت المؤسسات تحتاج SAML؟

صدر SAML 2.0 من منظمة OASIS في مارس 2005، أي قبل OpenID Connect بتسع سنوات، وخلال هذه السنوات بنت عليه الشركات الكبيرة والجامعات والجهات الحكومية أنظمة الدخول الموحد كلها. لذلك ما زلت تجده في الأماكن التالية:

- **الخدمات السحابية للمؤسسات SaaS:** أغلبها يدعم SAML لأنه ما يطلبه قسم تقنية المعلومات في الشركات الكبيرة، وبعضها لا يدعم غيره. وفي [توثيق Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal-setup-sso?ref=arabroot.io) مثلاً يأتي SAML كأول طريقة لربط تطبيق خارجي بالدخول الموحد.
- **التطبيقات الداخلية القديمة:** أنظمة الموارد البشرية والمحاسبة والبوابات التي بنيت قبل 2015 تتكلم SAML فقط، واستبدالها مكلف.
- **الاتحادات Federations بين المؤسسات:** شبكات الجامعات البحثية مثلاً تتبادل ملفات Metadata لمئات المؤسسات، ويدخل الباحث بحساب جامعته إلى خدمات جامعة أخرى.

وقد يتساءل البعض: إذا كان OpenID Connect أحدث وأبسط، فلماذا لا نتخلى عن SAML؟ والإجابة أن القرار ليس بيدك غالباً، فالتطبيق هو الذي يحدد ما يدعمه. والقاعدة العملية بسيطة: إذا دعم التطبيق الاثنين فاختر OpenID Connect، والسبب أن مكتباته أبسط وأقل عرضة لأخطاء التحقق من XML كما سيأتي، وإذا لم يدعم إلا SAML فاستخدمه، فكل مزودي الهوية مفتوحي المصدر المعروفين مثل [authentik](https://docs.goauthentik.io/add-secure-apps/providers/saml/?ref=arabroot.io) و[Keycloak](https://www.keycloak.org/docs/latest/server%5Fadmin/index.html?ref=arabroot.io#con-saml-clients%5Fserver%5Fadministration%5Fguide) يعملون كمزود هوية SAML.

|                            | SAML 2.0                                 | OpenID Connect                                     |
| -------------------------- | ---------------------------------------- | -------------------------------------------------- |
| صيغة الرسالة               | XML موقع بمعيار XML Signature            | JSON داخل JWT موقع                                 |
| طريقة الوصول               | المتصفح يحمل الرسالة كاملة في نموذج POST | المتصفح يحمل كوداً قصيراً، والتوكنات تطلب من الخلف |
| تطبيقات الجوال وواجهات API | غير مناسب                                | مصمم لها                                           |
| حجم الرسالة في تجربتنا     | 10877 بايت base64                        | توكن JWT ببضع مئات من البايتات                     |
| أين تجده                   | تطبيقات المؤسسات والاتحادات الجامعية     | التطبيقات الحديثة وأغلب الأدوات مفتوحة المصدر      |

## المصطلحات التي سوف تجدها في كل شاشة إعداد

معيار SAML نفسه عدة وثائق، أهمها [SAML Core](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf?ref=arabroot.io) الذي يعرف الرسائل، و[SAML Bindings](https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf?ref=arabroot.io) الذي يعرف طريقة نقلها، و[SAML Profiles](https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf?ref=arabroot.io) الذي يعرف كيف تستخدم في الدخول عبر المتصفح. ومن هذه الوثائق تأتي المصطلحات التالية:

- **المعرف Entity ID:** اسم فريد لكل طرف، يكتب غالباً على شكل رابط مثل `https://wiki.example.com`، ولكنه مجرد اسم ولا يشترط أن يفتح في المتصفح. والتطبيق يعرف نفسه به في الطلب، والمزود يضعه في الرد كجمهور Audience.
- **ملف التعريف Metadata:** ملف XML يصف الطرف: معرفه، وعناوينه، وشهادة التوقيع Signing Certificate. وتبادل الملفين هو كل ما تحتاجه الثقة بين الطرفين.
- **عنوان استقبال الرد ACS:** اختصار Assertion Consumer Service، وهو العنوان في التطبيق الذي يستقبل الرد من المتصفح، والمزود يرسل إليه فقط العنوان المسجل عنده.
- **عنوان الدخول SSO Service:** العنوان في المزود الذي يستقبل طلب الدخول.
- **طرق النقل Bindings:** كيف تحمل الرسالة: `HTTP-Redirect` يضعها مضغوطة داخل الرابط، وهو مناسب للطلب الصغير، و`HTTP-POST` يضعها في نموذج HTML يرسل نفسه، وهو المستخدم للرد الكبير، و`HTTP-Artifact` يرسل مرجعاً قصيراً ويجلب الطرف الآخر الرسالة من الخلف، وهو نادر.
- **الطلب AuthnRequest والرد Response:** رسالة التطبيق التي يطلب بها الدخول، ورسالة المزود التي تحمل الـ Assertion.
- **الـ Assertion:** الجزء الموقع الذي يقول من المستخدم ومتى تحقق منه وما صفاته، وهو قلب SAML كله.
- **المعرف NameID وصيغته Format:** هوية المستخدم داخل الـ Assertion، وقد تكون البريد `emailAddress`، أو معرفاً ثابتاً لا يتغير `persistent`، أو معرفاً عشوائياً لكل دخول `transient`.
- **الصفات Attributes:** بقية المعلومات مثل الاسم والبريد والمجموعات، ويتفق الطرفان على أسمائها.
- **RelayState:** قيمة يرسلها التطبيق مع الطلب ويعيدها المزود كما هي، ويستخدمها التطبيق ليعيد المستخدم إلى الصفحة التي طلبها أولاً.

## خطوات الدخول بالترتيب

في الطريقة المعتادة يبدأ التطبيق الدخول SP-initiated، والصورة التالية تبين الخطوات كما حدثت في تجربتنا، ولاحظ أن التطبيق والمزود لا يتصلان ببعضهما مباشرة أبداً في هذا التدفق، فالمتصفح هو الذي يحمل الرسالتين:

![مخطط تسلسلي بثلاثة أعمدة: التطبيق SP والمتصفح ومزود الهوية IdP. تفتح سارة الصفحة، فيرد التطبيق بالتحويل 303 ومعه AuthnRequest داخل الرابط بطريقة HTTP-Redirect، فيطلب المتصفح SingleSignOnService من المزود، وتدخل سارة كلمة المرور، فيرد المزود بنموذج HTML يرسل SAMLResponse وRelayState، فيرسله المتصفح بطريقة HTTP-POST إلى ACS، ثم يرد التطبيق بالتحويل 303 ومعه جلسته](https://arabroot.io/content/images/2026/10/saml-01-flow.webp)

الطلب صغير فيمر داخل الرابط، والرد كبير فيمر في نموذج POST

## تجربة عملية: مزود هوية وتطبيق على SimpleSAMLphp

سوف نستخدم [SimpleSAMLphp 2.5.3.1](https://github.com/simplesamlphp/simplesamlphp/releases/tag/v2.5.3.1?ref=arabroot.io) لأنه يعمل كمزود هوية وكتطبيق في نفس الوقت، وهو مشروع قديم ونشط بترخيص LGPL-2.1، وصدر منه في أغسطس 2026 الإصداران 2.5.3.1 و2.4.10\. وسوف نشغل منه نسختين في حاويتين منفصلتين: مزود هوية على المنفذ `18803` باسم `https://idp.example.com`، وتطبيقاً تجريبياً على المنفذ `18804` باسم `https://wiki.example.com`، وكل منهما احتاج في تجربتنا إلى قرابة 55 ميجابايت من الذاكرة. ولاحظ أن هذا الإعداد للتعلم فقط، فالتجربة تعمل عبر HTTP، وفي الإنتاج يجب أن يعمل الطرفان عبر HTTPS.

### الخطوة 1: الـ Image والمفاتيح

نحمل النسخة الكاملة من صفحة الإصدار، والتي تأتي ومعها مكتبات PHP، ونفكها في مجلد `dl`، ثم نبني Image فوق Image PHP الرسمية مع Apache بهذا الملف `Dockerfile`:

```dockerfile
FROM php:8.4.27-apache-trixie
COPY dl/simplesamlphp-2.5.3.1 /var/simplesamlphp
RUN printf '%s\n' 'SetEnv SIMPLESAMLPHP_CONFIG_DIR /var/simplesamlphp/config' 'Alias /simplesaml /var/simplesamlphp/public' '<Directory /var/simplesamlphp/public>' '  Require all granted' '</Directory>' > /etc/apache2/conf-enabled/simplesamlphp.conf \
 && mkdir -p /var/cache/simplesamlphp && chown -R www-data /var/simplesamlphp /var/cache/simplesamlphp
```

ثم نولد زوجاً من المفاتيح لكل طرف، فمفتاح المزود يوقع به الردود، ومفتاح التطبيق يوقع به طلباته إذا أردنا ذلك:

```bash
curl -sLO https://github.com/simplesamlphp/simplesamlphp/releases/download/v2.5.3.1/simplesamlphp-2.5.3.1-full.tar.gz
mkdir -p dl idp/{config,metadata,cert} sp/{config,metadata,cert,app}
tar xzf simplesamlphp-2.5.3.1-full.tar.gz -C dl
openssl req -x509 -newkey rsa:3072 -nodes -days 365 -subj "/CN=idp.example.com" -keyout idp/cert/server.pem -out idp/cert/server.crt
openssl req -x509 -newkey rsa:3072 -nodes -days 365 -subj "/CN=sp.example.com" -keyout sp/cert/server.pem -out sp/cert/server.crt
cp idp/cert/server.crt sp/cert/idp.crt
docker build -t saml-lab:2.5.3.1 .
```

لاحظ السطر الأخير قبل البناء: التطبيق يحصل على شهادة المزود العامة فقط `idp.crt`، ولا يرى مفتاحه الخاص أبداً، وهذه الشهادة هي ما سيتحقق به من كل رد.

### الخطوة 2: إعداد الطرفين

نسخنا ملف الإعداد الافتراضي `config.php.dist` من المشروع إلى `base.php` في مجلد الإعداد لكل طرف، ثم كتبنا `config.php` صغيراً يغير ما نحتاجه فقط. وهذا ملف المزود، والفرق في ملف التطبيق هو المنفذ `18804` وأسماء ملفات تعريف الارتباط بالبادئة `SP` بدلاً من `IDP`، والقيمة `false` في السطرين الأخيرين:

```php
<?php
require __DIR__ . '/base.php';
$config['baseurlpath'] = 'http://localhost:18803/simplesaml/';
$config['secretsalt'] = 'output of: openssl rand -hex 24';
$config['auth.adminpassword'] = 'output of: openssl rand -hex 12';
$config['technicalcontact_email'] = 'admin@example.com';
$config['timezone'] = 'UTC';
$config['logging.handler'] = 'errorlog';
$config['logging.level'] = SimpleSAML\Logger::INFO;
$config['session.cookie.name'] = 'IDPSESSID';
$config['session.phpsession.cookiename'] = 'IDPPHPSESS';
$config['session.authtoken.cookiename'] = 'IDPAuthToken';
$config['enable.saml20-idp'] = true;
$config['module.enable']['exampleauth'] = true;
```

غيرنا أسماء ملفات تعريف الارتباط لأن الطرفين يعملان على `localhost`، والمتصفح لا يفرق بين المنافذ في ملفات تعريف الارتباط، فلو تركنا الأسماء الافتراضية لاختلطت جلسة المزود بجلسة التطبيق. وفي الإنتاج لكل طرف نطاقه الخاص فلا تظهر هذه المشكلة.

بعد ذلك نعرف في المزود مصدر المستخدمين، وهو هنا قائمة ثابتة فيها سارة وصفاتها، وفي الواقع يكون LDAP أو قاعدة بيانات، في الملف `idp/config/authsources.php`:

```php
<?php
$config = [
    'admin' => ['core:AdminPassword'],
    'example-userpass' => [
        'exampleauth:UserPass',
        'users' => [
            'sara:Sara-Lab-2026' => [
                'uid' => ['sara'],
                'mail' => ['sara@example.com'],
                'displayName' => ['Sara Ali'],
                'groups' => ['finance', 'wiki-editors'],
            ],
        ],
    ],
];
```

ثم نعرف المزود نفسه في `idp/metadata/saml20-idp-hosted.php`: معرفه ومفتاحه، ومن أين يتحقق من المستخدمين، وأن NameID هو البريد:

```php
<?php
$metadata['https://idp.example.com'] = [
    'host' => '__DEFAULT__',
    'privatekey' => 'server.pem',
    'certificate' => 'server.crt',
    'auth' => 'example-userpass',
    'authproc' => [
        10 => [
            'class' => 'saml:AttributeNameID',
            'identifyingAttribute' => 'mail',
            'Format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
        ],
    ],
];
```

ونسجل التطبيق عند المزود في `idp/metadata/saml20-sp-remote.php`، وهذا ما تفعله في أي مزود هوية عندما تضيف تطبيق SAML:

```php
<?php
$metadata['https://wiki.example.com'] = [
    'AssertionConsumerService' => [[
        'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
        'Location' => 'http://localhost:18804/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp',
    ]],
    'SingleLogoutService' => [[
        'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
        'Location' => 'http://localhost:18804/simplesaml/module.php/saml/sp/saml2-logout.php/default-sp',
    ]],
    'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
];
```

ومن جهة التطبيق نعرفه في `sp/config/authsources.php` مع معرف المزود الذي يثق به:

```php
<?php
$config = [
    'admin' => ['core:AdminPassword'],
    'default-sp' => [
        'saml:SP',
        'entityID' => 'https://wiki.example.com',
        'idp' => 'https://idp.example.com',
        'privatekey' => 'server.pem',
        'certificate' => 'server.crt',
        'NameIDPolicy' => ['Format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', 'AllowCreate' => true],
    ],
];
```

ونسجل المزود عند التطبيق في `sp/metadata/saml20-idp-remote.php`، ولاحظ الشهادة `idp.crt`:

```php
<?php
$metadata['https://idp.example.com'] = [
    'SingleSignOnService' => [[
        'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
        'Location' => 'http://localhost:18803/simplesaml/module.php/saml/idp/singleSignOnService',
    ]],
    'SingleLogoutService' => [[
        'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
        'Location' => 'http://localhost:18803/simplesaml/module.php/saml/idp/singleLogout',
    ]],
    'certificate' => 'idp.crt',
];
```

وأخيراً صفحة التطبيق نفسها `sp/app/index.php`، وهي تطلب مستخدماً مسجلاً وتطبع ما وصلها عنه:

```php
<?php
// The "wiki": a page that needs a logged-in user. SimpleSAMLphp's SP library does the SAML work.
require_once '/var/simplesamlphp/src/_autoload.php';
$as = new \SimpleSAML\Auth\Simple('default-sp');
$as->requireAuth();
header('Content-Type: text/plain; charset=utf-8');
echo "Hello " . $as->getAttributes()['displayName'][0] . "\n";
echo "NameID: " . $as->getAuthData('saml:sp:NameID')->getValue() . "\n";
foreach ($as->getAttributes() as $k => $v) echo "  $k = " . implode(', ', $v) . "\n";
```

في الإعداد أعلاه لاحظ التالي:

- عنوان ACS في `saml20-sp-remote.php` هو العنوان الوحيد الذي سيقبل المزود أن يرسل إليه الرد، فإذا جاءه طلب يطلب عنواناً آخر رفضه.
- التطبيق لا يحتاج إلى أي شيء عن المستخدمين، فكل ما يعرفه هو معرف المزود وعنوانه وشهادته.
- في الإنتاج لا تكتب هذه الملفات بيدك عادة، وإنما تعطي كل طرف رابط Metadata الطرف الآخر، والمزود في تجربتنا ينشر ملفه على `/simplesaml/module.php/saml/idp/metadata`.

### الخطوة 3: التشغيل

```bash
docker network create saml-lab
docker run -d --name saml-idp --network saml-lab -p 18803:80 \
  -v "$PWD/idp/config:/var/simplesamlphp/config" -v "$PWD/idp/metadata:/var/simplesamlphp/metadata" -v "$PWD/idp/cert:/var/simplesamlphp/cert" \
  saml-lab:2.5.3.1
docker run -d --name saml-sp --network saml-lab -p 18804:80 \
  -v "$PWD/sp/config:/var/simplesamlphp/config" -v "$PWD/sp/metadata:/var/simplesamlphp/metadata" -v "$PWD/sp/cert:/var/simplesamlphp/cert" \
  -v "$PWD/sp/app/index.php:/var/www/html/index.php" \
  saml-lab:2.5.3.1
```

إذا فتحت `http://localhost:18804/` في المتصفح فسوف يحولك التطبيق إلى صفحة دخول المزود:

![صفحة دخول SimpleSAMLphp بعنوان Enter your username and password وفيها حقلا Username وPassword وزر Login](https://arabroot.io/content/images/2026/10/saml-03-idp-login.webp)

صفحة الدخول في المزود على المنفذ 18803، وكلمة المرور تكتب هنا وليس في التطبيق

⚠️

إذا ظهرت صفحة خطأ وفي السجل `Unable to create the "cache" directory (/var/cache/simplesamlphp/saml)` فالسبب أن المجلد غير موجود أو لا يملكه مستخدم Apache، وهو سبب السطر `mkdir -p /var/cache/simplesamlphp` في الـ Dockerfile. وإذا ظهر `Endpoint of type AssertionConsumerService is not an array` فالإصدار 2 يطلب أن يكون ACS مصفوفة فيها `Binding` و`Location` وليس نصاً.

### الخطوة 4: نمشي الدخول بأنفسنا مع curl

حتى نرى الرسائل نفسها نستخدم `curl` مكان المتصفح، مع ملف يحفظ ملفات تعريف الارتباط بين الطلبات. ونحتاج أولاً إلى سكربت صغير يفك رسائل SAML، وهو `decode.py`:

```python
# decode.py: print a SAML message readable. Redirect binding = deflate+base64+urlencode; POST binding = base64 only.
import sys, base64, zlib, urllib.parse, xml.dom.minidom
v = urllib.parse.unquote(sys.argv[1]) if '%' in sys.argv[1] else sys.argv[1]
raw = base64.b64decode(v)
try: raw = zlib.decompress(raw, -15)
except zlib.error: pass
print(xml.dom.minidom.parseString(raw).toprettyxml(indent='  ').split('\n', 1)[1].strip())
```

ثم نكتب الخطوات في `flow.sh`: نفتح التطبيق، ونتبع التحويل إلى المزود، ونرسل كلمة المرور، ونحفظ الرد:

```bash
#!/usr/bin/env bash
# One SP-initiated SAML login with curl as the browser. Prints each hop, saves request.xml and response.xml.
SP=http://localhost:18804; J=${JAR:-jar.txt}; rm -f $J
echo "== 1. open the wiki"
curl -s -c $J -b $J -D r1.txt -o /dev/null $SP/; grep -iE '^(HTTP|location)' r1.txt | cut -c1-120
LOC=$(grep -i '^location' r1.txt | cut -d' ' -f2 | tr -d '\r')
python -I decode.py "$(echo "$LOC" | sed 's/.*SAMLRequest=\([^&]*\).*/\1/')" > request.xml
echo "== 2. the IdP asks for a password"
curl -s -c $J -b $J -L -D r2.txt -o login.html "$LOC"; grep -iE '^(HTTP|location)' r2.txt | cut -c1-120
ACT=$(grep -o 'id="f" class="[^"]*" action="[^"]*"' login.html | sed 's/.*action="//;s/"$//;s/&amp;/\&/g')
echo "== 3. sara logs in, the IdP answers with an auto-post form"
curl -s -c $J -b $J -L -o resp.html -w 'HTTP %{http_code}\n' --data-urlencode username=sara --data-urlencode password=Sara-Lab-2026 "$ACT"
grep -o '<form[^>]*>' resp.html
grep -o 'name="SAMLResponse" value="[^"]*"' resp.html | sed 's/.*value="//;s/"$//' > samlresponse.b64
grep -o 'name="RelayState" value="[^"]*"' resp.html | sed 's/.*value="//;s/"$//' > relaystate.txt
echo "SAMLResponse: $(wc -c < samlresponse.b64) bytes of base64"
python -I decode.py "$(cat samlresponse.b64)" > response.xml
```

والخطوة الأخيرة هي ما يفعله نموذج HTML في المتصفح تلقائياً، أي إرسال الرد إلى ACS، ونضعها في `post.sh` لأننا سوف نستخدمها في تجارب الأعطال أيضاً:

```bash
#!/usr/bin/env bash
# post.sh <file-with-base64-response>: what the browser's auto-post form does
J=${JAR:-jar.txt}
curl -s -c $J -b $J -D p.txt -o p.html --data-urlencode "SAMLResponse=$(cat $1)" --data-urlencode "RelayState=$(cat relaystate.txt | sed 's/&amp;/\&/g')" http://localhost:18804/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp
grep -iE '^(HTTP|location)' p.txt | cut -c1-120
```

ونشغل الخطوات ثم نفتح الصفحة:

```bash
bash flow.sh
bash post.sh samlresponse.b64
curl -s -c jar.txt -b jar.txt http://localhost:18804/
```

والمخرج سوف يكون كما يلي:

```text
== 1. open the wiki
HTTP/1.1 303 See Other
Location: http://localhost:18803/simplesaml/module.php/saml/idp/singleSignOnService?SAMLRequest=fVLJbgIxDP2VUe6zspSJAImC
== 2. the IdP asks for a password
HTTP/1.1 303 See Other
Location: http://localhost:18803/simplesaml/module.php/core/loginuserpass?AuthState=_7ceaa9974269d290f845308d180188f70bd
HTTP/1.1 200 OK
== 3. sara logs in, the IdP answers with an auto-post form
HTTP 200
<form method="post" action="http://localhost:18804/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp">
SAMLResponse: 10877 bytes of base64
HTTP/1.1 303 See Other
Location: http://localhost:18804/
Hello Sara Ali
NameID: sara@example.com
  uid = sara
  mail = sara@example.com
  displayName = Sara Ali
  groups = finance, wiki-editors
```

وهذا هو الطلب الذي حمله الرابط في الخطوة الأولى بعد فكه (`cat request.xml`):

```xml
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_ca6fca80d8036e1c039dcee05e99ce3bb7b925f12b" Version="2.0" IssueInstant="2026-10-25T19:36:14Z" Destination="http://localhost:18803/simplesaml/module.php/saml/idp/singleSignOnService" AssertionConsumerServiceURL="http://localhost:18804/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">
  <saml:Issuer>https://wiki.example.com</saml:Issuer>
  <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true"/>
</samlp:AuthnRequest>
```

في الطلب أعلاه لاحظ التالي:

- `ID` معرف عشوائي يحفظه التطبيق في جلسة المتصفح، وسوف يعود في الرد باسم `InResponseTo`.
- `AssertionConsumerServiceURL` هو المكان الذي يريد التطبيق أن يصله الرد، والمزود يقارنه بما سجلناه عنده.
- الطلب غير موقع في تجربتنا، وهذا مقبول لأن المزود لا يرسل الرد إلا إلى ACS المسجل، ولكن بعض المزودين يطلبون توقيع الطلب أيضاً.

## الـ Assertion الحقيقي، وماذا يفحص التطبيق فيه

والآن أهم ما في المقال: الرد الذي حمله المتصفح في الخطوة الثالثة، كما فكه `decode.py`، بعد أن اختصرنا قيم التوقيع والشهادات فقط لأنها مئات الأحرف:

```xml
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_3b42faa8727002116dba4db606d0227477de9fce5d" Version="2.0" IssueInstant="2026-10-25T19:36:16Z" Destination="http://localhost:18804/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp" InResponseTo="_ca6fca80d8036e1c039dcee05e99ce3bb7b925f12b">
  <saml:Issuer>https://idp.example.com</saml:Issuer>
  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
    <ds:SignedInfo>
      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
      <ds:Reference URI="#_3b42faa8727002116dba4db606d0227477de9fce5d">
        <ds:Transforms>
          <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
        </ds:Transforms>
        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
        <ds:DigestValue>Pe0pvOqbZ62Wa6wNtotNOHb0pdHDkmPg2/KPJufgGaI=</ds:DigestValue>
      </ds:Reference>
    </ds:SignedInfo>
    <ds:SignatureValue>…</ds:SignatureValue>
    <ds:KeyInfo>
      <ds:X509Data>
        <ds:X509Certificate>MIIE…</ds:X509Certificate>
      </ds:X509Data>
    </ds:KeyInfo>
  </ds:Signature>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_be660efdab841f6ba2bf82e3dab20fb2d3c38371f5" Version="2.0" IssueInstant="2026-10-25T19:36:16Z">
    <saml:Issuer>https://idp.example.com</saml:Issuer>
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
      <ds:SignedInfo>
        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
        <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
        <ds:Reference URI="#_be660efdab841f6ba2bf82e3dab20fb2d3c38371f5">
          <ds:Transforms>
            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          </ds:Transforms>
          <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
          <ds:DigestValue>m8UcPcGlwXFs37IDHlwO4Y45vBEVLJIh4l37nbrqvRY=</ds:DigestValue>
        </ds:Reference>
      </ds:SignedInfo>
      <ds:SignatureValue>…</ds:SignatureValue>
      <ds:KeyInfo>
        <ds:X509Data>
          <ds:X509Certificate>MIIE…</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </ds:Signature>
    <saml:Subject>
      <saml:NameID SPNameQualifier="https://wiki.example.com" Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">sara@example.com</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData NotOnOrAfter="2026-10-25T19:41:16Z" Recipient="http://localhost:18804/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp" InResponseTo="_ca6fca80d8036e1c039dcee05e99ce3bb7b925f12b"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="2026-10-25T19:35:46Z" NotOnOrAfter="2026-10-25T19:41:16Z">
      <saml:AudienceRestriction>
        <saml:Audience>https://wiki.example.com</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="2026-10-25T19:36:16Z" SessionNotOnOrAfter="2026-10-26T03:36:16Z" SessionIndex="_a79c4196de2c7218e09cd89073d831187756757200">
      <saml:AuthnContext>
        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
      </saml:AuthnContext>
    </saml:AuthnStatement>
    <saml:AttributeStatement>
      <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
        <saml:AttributeValue xsi:type="xs:string">sara</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
        <saml:AttributeValue xsi:type="xs:string">sara@example.com</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="displayName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
        <saml:AttributeValue xsi:type="xs:string">Sara Ali</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="groups" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
        <saml:AttributeValue xsi:type="xs:string">finance</saml:AttributeValue>
        <saml:AttributeValue xsi:type="xs:string">wiki-editors</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>
```

والصورة التالية تضع كل جزء من هذا الرد بجانب الفحص الذي يجريه التطبيق عليه:

![خمسة أجزاء من الـ Assertion الحقيقي بجانب ما يفحصه التطبيق: Issuer والتوقيع يتحقق منهما بالشهادة المحفوظة، وNameID مع Recipient وInResponseTo، وConditions مع NotBefore وNotOnOrAfter وAudience، وAuthnStatement مع AuthnInstant وSessionIndex، والصفات mail وgroups](https://arabroot.io/content/images/2026/10/saml-02-assertion.webp)

كل حقل في الـ Assertion يقابله فحص، وإذا سقط فحص واحد يجب أن يرفض التطبيق الرسالة

في الرد أعلاه لاحظ التالي:

- **توقيعان وليس توقيعاً واحداً:** المزود وقع الرد كله `Response`، ووقع الـ Assertion داخله أيضاً، وكل توقيع يشير بالحقل `Reference URI` إلى معرف الجزء الذي يغطيه. وهذا التفصيل هو أصل أخطر ثغرات SAML كما سيأتي.
- **الشهادة داخل الرسالة لا تعني شيئاً:** الرسالة تحمل شهادة المزود في `X509Certificate`، ولكن التطبيق الصحيح يتحقق بالشهادة التي سجلتها عنده مسبقاً، وإلا استطاع أي شخص أن يوقع رسالة بمفتاحه ويرفق شهادته.
- **مدة قصيرة جداً:** بين `NotBefore` و`NotOnOrAfter` في `Conditions` خمس دقائق ونصف، فالمزود أصدر الرد في `19:36:16` وجعل بدايته قبل ذلك بثلاثين ثانية حتى يتحمل فرق الساعة بين السيرفرين، وجعل نهايته بعد خمس دقائق. لذلك يجب أن تكون ساعة السيرفرين مضبوطة عبر NTP، وإلا رفضت ردود صحيحة.
- **Audience وRecipient:** الرد صادر لـ `https://wiki.example.com` فقط وإلى عنوان ACS الخاص به فقط، فلا يصلح لتطبيق آخر حتى لو كان يثق بنفس المزود.
- **SessionNotOnOrAfter:** المزود يقترح على التطبيق ألا تزيد جلسته عن ثماني ساعات، والتطبيق حر في الالتزام بذلك.
- **الصفات تمر كما هي:** `groups` تحمل `finance` و`wiki-editors`، والتطبيق يبني عليها الصلاحيات، ولذلك يجب أن يرفض أي تعديل عليها.

## نجرب الأعطال عن قصد

الرسالة الموقعة تكون آمنة بقدر الفحوص التي يجريها التطبيق عليها، لذلك نجرب الآن أربع حالات يحاول فيها مهاجم أن يستفيد من رد حقيقي.

### ماذا يحدث إذا غير أحد المجموعة داخل الرد؟

نسجل الدخول من جديد ونحتفظ بالرد قبل إرساله، ثم نغير المجموعة `finance` إلى `admins` بهذا السكربت `tamper.py`:

```python
# tamper.py <in.b64> <out.b64>: change sara's group inside the signed assertion
import sys, base64
xml = base64.b64decode(open(sys.argv[1]).read())
print('finance found:', xml.count(b'>finance<'))
open(sys.argv[2], 'w').write(base64.b64encode(xml.replace(b'>finance<', b'>admins<')).decode())
```

```bash
bash flow.sh > /dev/null
python tamper.py samlresponse.b64 tampered.b64
bash post.sh tampered.b64
docker logs saml-sp 2>&1 | grep 'Caused by' | tail -1
```

```text
finance found: 1
HTTP/1.0 500 Internal Server Error
simplesamlphp ERR [b0b3165b7a] Caused by: Exception: Reference validation failed
```

الهاش المحسوب للـ Assertion بعد التعديل لم يعد يطابق `DigestValue` الموقع، فرفض التطبيق الرد بالكامل. ثم أرسلنا الرد الأصلي غير المعدل من نفس المتصفح فقبله التطبيق وظهرت صفحة `Hello Sara Ali`، أي أن الرفض كان بسبب التعديل فقط.

### ماذا يحدث إذا سرق أحدهم الرد وأرسله من متصفحه؟

لنفرض أن المهاجم حصل على الرد الذي استخدمته سارة، من سجل Proxy أو من ملف HAR أرسلته للدعم الفني، فأرسله من متصفح آخر بملف ملفات تعريف ارتباط جديد:

```bash
rm -f attacker.txt
JAR=attacker.txt bash post.sh main.b64
```

```text
HTTP/1.0 500 Internal Server Error
simplesamlphp ERR [ff7493b1c9] Caused by: SimpleSAML\Error\Exception: Unsolicited Response MUST NOT contain InResponseTo. Got: '_ca6fca80d8036e1c039dcee05e99ce3bb7b925f12b'
```

هنا يظهر دور `InResponseTo`: الرد يقول إنه جواب على الطلب `_ca6f…`، ولكن جلسة متصفح المهاجم لم ترسل هذا الطلب، فرفضه التطبيق. وهذا الفحص هو ما يجعل الدخول الذي يبدؤه التطبيق أقوى من الدخول الذي يبدؤه المزود IdP-initiated، والسبب أن الرد في الحالة الثانية لا يحمل `InResponseTo` أصلاً، فلا يبقى للتطبيق إلا المدة القصيرة وفحص التكرار.

### ماذا يحدث إذا وصل الرد بعد انتهاء مدته؟

سجلنا الدخول في `19:37:14` فكان `NotOnOrAfter` هو `19:42:13`، ثم انتظرنا حتى `19:43:55` وأرسلنا الرد من نفس المتصفح:

```text
HTTP/1.0 500 Internal Server Error
simplesamlphp ERR [84f5f540ca] Caused by: SimpleSAML\Error\Exception: Error validating SubjectConfirmation in Assertion: NotOnOrAfter in SubjectConfirmationData is in the past: 1792957333
```

### وماذا عن إرسال الرد نفسه مرتين؟

عندما أرسلنا الرد الأصلي مرة ثانية من متصفح سارة نفسه لم يظهر خطأ، وإنما أعاد التطبيق التحويل `303` إلى الصفحة، وكتب في سجله:

```text
simplesamlphp INFO [33c207836b] Duplicate SAML 2 response detected - ignoring the response and redirecting the user to the correct page.
```

أي أن التطبيق يحفظ معرفات الردود التي استخدمها، ويتجاهل الرد المكرر دون أن ينشئ جلسة جديدة منه. وهذا الفحص مطلوب في [SAML Profiles](https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf?ref=arabroot.io) لأن الـ Assertion من نوع Bearer، أي أن من يحمله يدخل به، تماماً مثل التذكرة.

## ثغرات SAML المعروفة: لماذا لا تكتب التحقق بنفسك

التحقق من توقيع XML أصعب بكثير مما يبدو، والسبب أن معيار [XML Signature](https://www.w3.org/TR/xmldsig-core1/?ref=arabroot.io) يوقع جزءاً من الوثيقة يشار إليه بمعرف، وليس الوثيقة كلها، ثم تقرأ المكتبة البيانات من الوثيقة. فإذا كانت الطريقة التي تحدد بها المكتبة الجزء الموقع تختلف عن الطريقة التي تقرأ بها البيانات، استطاع المهاجم أن يضع Assertion موقعاً حقيقياً في مكان، وAssertion مزوراً في مكان آخر، فيتحقق التوقيع من الأول ويقرأ التطبيق الثاني. وهذا ما يسمى تغليف التوقيع XML Signature Wrapping، وتكرر في مكتبات كثيرة.

📌

في سبتمبر 2024 نشرت مكتبة ruby-saml الثغرة [CVE-2024-45409](https://github.com/advisories/GHSA-jw9c-mfg7-9rx2?ref=arabroot.io) بدرجة 9.9 من 10، وهي أن المكتبة لا تتحقق من توقيع الرد بشكل صحيح، فمن يملك أي وثيقة موقعة من مزود الهوية يستطيع أن يعدلها ويدخل باسم مستخدم آخر. وكانت GitLab من أشهر من يستخدم المكتبة، فأصدرت في [17 سبتمبر 2024](https://docs.gitlab.com/releases/patches/patch-release-gitlab-17-3-3-released/?ref=arabroot.io) إصدارات تصحيحية لخمسة فروع، ونصحت حتى التحديث بتفعيل المصادقة الثنائية في GitLab نفسه لكل الحسابات، ونبهت إلى أن تفعيل MFA في مزود الهوية لا يحمي من هذه الثغرة، والسبب أن المهاجم لا يمر على المزود أصلاً. ثم وجد فريق GitHub Security Lab في 2025 طريقة أخرى في نفس المكتبة من خلال [الفرق بين محللي XML](https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/?ref=arabroot.io). والدرس أن التطبيق الذي يستقبل SAML يجب أن يستخدم مكتبة مشهورة، وأن يحدثها فور صدور التحديث الأمني.

## قائمة تحقق قبل أن تربط تطبيقاً حقيقياً

- **اشترط توقيع الـ Assertion:** وليس الرد الخارجي فقط، فإذا كان التطبيق يسمح بخيار «لا تتحقق من التوقيع» فلا تستخدمه حتى في الاختبار.
- **الشهادة من الـ Metadata فقط:** تأكد أن التطبيق يتحقق بالشهادة المسجلة عنده، وليس بالشهادة المرفقة في الرسالة.
- **استخدم معرفاً ثابتاً:** اربط الحساب في التطبيق بـ NameID من نوع `persistent` أو بمعرف ثابت من الدليل، وتجنب البريد إذا كان قد يتغير أو يعاد استخدامه لموظف جديد.
- **ضبط الساعة:** شغل NTP على سيرفر المزود وسيرفر التطبيق، فالفرق الكبير يرفض ردوداً صحيحة.
- **دوران الشهادات Certificate Rotation:** شهادة التوقيع لها تاريخ انتهاء، والتطبيقات التي تقرأ الشهادة يدوياً سوف تتوقف في يوم انتهائها. لذلك ضع الشهادة الجديدة في الـ Metadata بجانب القديمة قبل التبديل بأسابيع، أو اجعل التطبيق يقرأ Metadata المزود من رابطها دورياً.
- **التشفير Encryption اختياري:** يمكن أن يشفر المزود الـ Assertion بمفتاح التطبيق العام، وهذا يفيد عندما تمر الردود عبر أطراف لا تثق بها، ولكنه لا يغني عن HTTPS ولا عن التحقق من التوقيع.
- **الخروج الموحد Single Logout:** SAML يدعمه، ولكن دعمه في التطبيقات ضعيف، فاجعل مدة جلسة التطبيق قصيرة كما ذكرنا في [شرح الخروج الموحد](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%88%D8%AD%D8%AF-sso-%D9%88%D8%A7%D9%84%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA/#logout).
- **لا ترسل ملفات HAR وفيها ردود SAML:** الرد صالح لدقائق فقط ومربوط بالجلسة، ولكن نفس الملف يحمل ملفات تعريف الارتباط التي تعمل ساعات.

## رسائل خطأ شائعة وما تعنيه

| ما تراه                                            | السبب الأرجح                                                                                                                                 |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Reference validation failed أو Invalid signature   | الرد معدل، أو الشهادة المسجلة في التطبيق ليست شهادة المزود الحالية بعد تجديدها                                                               |
| NotOnOrAfter … is in the past                      | الرد وصل متأخراً، أو ساعة أحد السيرفرين غير مضبوطة                                                                                           |
| Unsolicited Response MUST NOT contain InResponseTo | الرد وصل إلى متصفح أو جلسة غير التي طلبته، وأشهر سبب بريء له اختلاط ملفات تعريف الارتباط بين نطاقين أو انتهاء جلسة التطبيق قبل عودة المستخدم |
| المزود يرفض الطلب بسبب ACS                         | عنوان ACS في الطلب لا يطابق المسجل عند المزود، وغالباً بسبب http بدلاً من https خلف Reverse Proxy                                            |
| Audience غير صحيح                                  | Entity ID المكتوب في المزود يختلف حرفاً عن الذي يعرف به التطبيق نفسه، مثل شرطة مائلة في النهاية                                              |

## إزالة التجربة

```bash
docker rm -f saml-idp saml-sp
docker network rm saml-lab
docker rmi saml-lab:2.5.3.1
```

[![](https://arabroot.io/content/images/2026/10/sso-basics-cover-cover.webp)دليلما هو الدخول الموحد SSO؟ المصطلحات التي تحتاجها أولاًخريطة لمصطلحات الدخول الموحد SSO مع تجربة حقيقية تبين كيف تفتح جلسة واحدة في مزود الهوية تطبيقين، وماذا يبقى بعد الخروج.](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%88%D8%AD%D8%AF-sso-%D9%88%D8%A7%D9%84%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/oauth-oidc-cover-cover.webp)دليلشرح OAuth وOpenID Connect للمبتدئينلماذا لا يجب أن يعرف أي تطبيق كلمة مرورك، وكيف يحل OAuth ذلك بتوكنات محدودة، وكيف يضيف OpenID Connect الدخول الموحد.](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/authentik-cover-cover-1.webp)دليلتثبيت authentik للدخول الموحد SSO إلى تطبيقاتككيف تثبت authentik حتى يدخل كل موظف إلى تطبيقات الفريق بحساب واحد، مع فرض المصادقة الثنائية على الجميع.](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-authentik-%D9%84%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%88%D8%AD%D8%AF/)[![](https://arabroot.io/content/images/2026/10/ldap-directory-cover-cover.webp)دليلLDAP ودليل المستخدمين: كيف تعرف التطبيقات من هو المستخدم؟نبني دليل مستخدمين على OpenLDAP ونرى بمخرجات ldapsearch الحقيقية كيف تتحقق التطبيقات من كلمة المرور والمجموعات، وأين يخطئ أغلب من يبدأ.](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-ldap-%D9%88%D8%AF%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%AE%D8%AF%D9%85%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/scim-cover-cover.webp)دليلما هو SCIM؟ إدارة حسابات الموظفين تلقائياً في كل تطبيقاتكالدخول الموحد SSO يمنع الموظف المغادر من الدخول لكن حساباته تبقى نشطة، وSCIM ينشئ الحسابات ويعطلها في كل تطبيق تلقائياً.](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-scim-%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%AD%D8%B3%D8%A7%D8%A8%D8%A7%D8%AA-%D8%AA%D9%84%D9%82%D8%A7%D8%A6%D9%8A%D8%A7/)

## الخلاصة

- SAML 2.0 معيار من 2005 ما زالت تعتمد عليه تطبيقات المؤسسات والخدمات السحابية والاتحادات الجامعية، فإذا دعم التطبيق OpenID Connect فاختره، وإذا لم يدعم إلا SAML فاستخدمه دون تردد.
- الثقة تبنى بتبادل ملفات Metadata: معرف كل طرف Entity ID، وعنوان ACS في التطبيق، وشهادة التوقيع في المزود.
- الطلب صغير فيمر داخل الرابط HTTP-Redirect، والرد كبير فيمر في نموذج HTTP-POST، وفي تجربتنا كان الرد 10877 بايت base64 فيه توقيعان.
- التطبيق يفحص التوقيع بالشهادة المسجلة، والجمهور Audience، والمستلم Recipient، والمدة، وInResponseTo، والتكرار. ورأينا في التجربة رفض الرد المعدل والرد المسروق والرد المنتهي، وتجاهل الرد المكرر.
- لا تكتب التحقق من XML بنفسك، فثغرات Signature Wrapping أصابت مكتبات معروفة مثل ruby-saml، واستخدم مكتبة مشهورة وحدثها فوراً.
- ضبط الساعة عبر NTP ودوران الشهادات قبل انتهائها يمنعان أغلب الأعطال المفاجئة في الإنتاج.

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ تجربة الدخول والأعطال الأربعة على SimpleSAMLphp 2.5.3.1 مع PHP 8.4.27.