> ## 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.

# شرح OAuth وOpenID Connect للمبتدئين
- URL: https://arabroot.io/articles/شرح-oauth-و-openid-connect-للمبتدئين/
- Published: 2026-10-05T09:00:00.000Z
- Updated: 2026-10-09T09:41:58.000Z
- Description: لماذا لا يجب أن يعرف أي تطبيق كلمة مرورك، وكيف يحل OAuth ذلك بتوكنات محدودة، وكيف يضيف OpenID Connect الدخول الموحد.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, الاستضافة الذاتية

كل يوم تضغط زراً مكتوباً عليه «الدخول بحساب Google» أو «Sign in with GitHub»، فتنتقل إلى صفحة Google وتكتب كلمة المرور هناك، ثم تعود إلى التطبيق وأنت داخل حسابك دون أن يعرف التطبيق كلمة مرورك، وإذا ثبتت authentik على سيرفرك كما في دليل [تثبيت 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/) فأنت تبني الشيء نفسه لفريقك، حيث يدخل الموظف مرة واحدة إلى authentik ثم يفتح Gitea وOutline وGrafana دون أن يكتب كلمة مرور جديدة. والذي يجعل هذا ممكناً بروتوكولان يعملان معاً: **OAuth 2.0** للتفويض Authorization، و**OpenID Connect** واختصاراً OIDC لإثبات هوية المستخدم Authentication.

وفي أدلتنا العملية طلبنا منك أن تنسخ Client ID وClient Secret، وأن تكتب Redirect URI حرفاً بحرف، وأن تختار Signing Key، وأن تضيف النطاق offline\_access، ومن الطبيعي أن تتساءل لماذا كل هذا، وما الذي يحدث فعلاً بين المتصفح والتطبيق ومزود الهوية. وهذا المقال يبدأ من قبل الأدلة بخطوة، أي من المشكلة نفسها، ولا نفترض فيه أنك تعرف أي شيء عن هذه البروتوكولات، وسوف نناقش في هذا المقال ما يلي:

- المشكلة التي جاء OAuth ليحلها، وكيف تطور من OAuth 1.0 إلى OAuth 2.1.
- الأطراف الأربعة والنطاقات Scopes والتوكنات Tokens، والفرق بين التفويض وتسجيل الدخول.
- تدفق Authorization Code خطوة خطوة مع PKCE والمعامل state ومطابقة redirect\_uri، ثم بقية التدفقات وما انتهى زمنه منها.
- OpenID Connect والـ ID Token، وكيف تقرأ JWT وتتحقق من توقيعه، والاكتشاف Discovery وJWKS.
- كيف يظهر كل هذا في تطبيقاتك المستضافة ذاتياً، وما معنى كل حقل في شاشة الإعداد.
- تجربة عملية بمزود هوية صغير هو Dex مع curl، نمشي فيها التدفق كاملاً ونكسره عمداً لنرى الأخطاء.
- أين تحفظ التوكنات، ومدة صلاحيتها، وتسجيل الخروج، ثم قائمة بأفضل الممارسات والأخطاء الشائعة، ومقارنة قصيرة مع SAML وLDAP.

## ما المشكلة التي جاء OAuth ليحلها؟

لنفرض أنك تستخدم خدمة لتخزين الصور، وظهر تطبيق لطباعة الصور يريد أن يقرأ ألبوماتك ليطبعها لك، فكيف يصل التطبيق إلى صورك؟ الحل الأول الذي استخدمته مواقع كثيرة في منتصف العقد الأول من هذا القرن كان بسيطاً جداً: يطلب منك التطبيق اسم المستخدم وكلمة المرور لحسابك في خدمة الصور، ثم يدخل بها نيابة عنك. وكانت مواقع التواصل في ذلك الوقت تطلب كلمة مرور بريدك الإلكتروني حتى تقرأ قائمة جهات الاتصال وتقترح عليك أصدقاء، وهذا النمط له اسم معروف هو نمط مشاركة كلمة المرور Password Anti-Pattern، وهو من أسوأ الطرق على الإطلاق للأسباب التالية:

- **الصلاحية كاملة:** التطبيق الذي يريد قراءة الصور يحمل الآن مفتاح حسابك كله، فيستطيع أن يقرأ بريدك ويحذف ملفاتك ويغير كلمة المرور نفسها، ولا توجد طريقة تقول بها «اقرأ الصور فقط».
- **كلمة المرور في مكان لا تملكه:** التطبيق يحتاج أن يحفظ كلمة مرورك كما هي حتى يستخدمها لاحقاً، أي أنها لا يمكن أن تحفظ كهاش Hash، فإذا اخترق سيرفر التطبيق خرجت كلمة مرورك معه، وإذا كنت تستخدمها في مواقع أخرى فقد خرجت تلك الحسابات أيضاً.
- **لا يمكن سحب الصلاحية من تطبيق واحد:** إذا أعطيت كلمة المرور لثلاثة تطبيقات وأردت إيقاف واحد منها، فالطريقة الوحيدة أن تغير كلمة المرور، وبالتالي تتوقف التطبيقات الثلاثة معاً.
- **لا يعمل مع المصادقة الثنائية:** إذا فعلت المصادقة الثنائية MFA على حسابك فالتطبيق لا يستطيع أن يدخل بكلمة المرور وحدها، لذلك كانت الخدمات تضطر إلى إنشاء «كلمات مرور للتطبيقات» App Passwords، وهي حل مؤقت لنفس المشكلة.
- **لا تعرف الخدمة من يدخل:** كل الطلبات تأتي باسمك، فلا تستطيع خدمة الصور أن تفرق بينك وبين تطبيق الطباعة، ولا أن تعرض لك قائمة بالتطبيقات التي تصل إلى حسابك.

وقد يتساءل البعض: لماذا لا نعطي التطبيق كلمة مرور ثانية خاصة به؟ والإجابة أن هذا هو بالضبط اتجاه الحل، ولكن بشكل منظم: أن يحصل كل تطبيق على مفتاح خاص به، محدود بما وافقت عليه، ولمدة محددة، وتستطيع أن تسحبه وحده، ودون أن يرى كلمة مرورك إطلاقاً. وهذا المفتاح هو التوكن Token، والصورة التالية تبين الفرق بين الحالتين:

![مخطط يقارن مستخدماً يعطي كلمة مروره لثلاثة تطبيقات يدخل كل منها إلى الحساب بصلاحية كاملة، بمستخدم يدخل في خادم التفويض فقط فيحصل التطبيق على توكن محدود بقراءة الصور لمدة ساعة ويمكن سحبه وحده](https://arabroot.io/content/images/2026/10/oauth-oidc-01-password-sharing.webp)

يمين: كلمة المرور نفسها عند كل تطبيق. يسار: كل تطبيق يأخذ توكن محدوداً ويمكن سحبه وحده

### من OAuth 1.0 إلى OAuth 2.1

بدأت فكرة OAuth في نوفمبر 2006 عندما كان مهندسو Twitter يعملون على دعم OpenID، واحتاجوا طريقة تسمح لتطبيقات أخرى بالوصول إلى حسابات المستخدمين دون كلمات مرورهم، كما يروي [تاريخ المشروع في موقع oauth.net](https://oauth.net/about/introduction/?ref=arabroot.io). وصدرت المواصفة الأولى [OAuth Core 1.0](https://oauth.net/core/1.0/?ref=arabroot.io) في 4 ديسمبر 2007، ثم أصبحت معياراً من IETF باسم [RFC 5849](https://datatracker.ietf.org/doc/html/rfc5849?ref=arabroot.io) في أبريل 2010.

وكان OAuth 1.0 يطلب من التطبيق أن يوقع كل طلب بتوقيع تشفيري معقد، فكان من الصعب تطبيقه بشكل صحيح، ولا يناسب تطبيقات الجوال، لذلك صدر [OAuth 2.0 في RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749?ref=arabroot.io) في أكتوبر 2012 ومعه [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750?ref=arabroot.io) الذي يشرح استخدام التوكن من نوع Bearer، وهو ليس تحديثاً للإصدار الأول وإنما إطار جديد لا يتوافق معه، يعتمد على HTTPS بدلاً من التوقيع في كل طلب. وفي فبراير 2014 أصدرت OpenID Foundation مواصفة [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1%5F0.html?ref=arabroot.io) التي تضيف طبقة الهوية فوق OAuth 2.0، وهي التي تستخدمها اليوم كل أزرار «الدخول بحساب…».

وبعد ذلك جاءت الإضافات التي سوف نمر عليها في هذا المقال: [PKCE في RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636?ref=arabroot.io) في سبتمبر 2015، و[تدفق الأجهزة في RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628?ref=arabroot.io) في أغسطس 2019، ثم [أفضل الممارسات الحالية لأمان OAuth 2.0 في RFC 9700](https://datatracker.ietf.org/doc/html/rfc9700?ref=arabroot.io) في يناير 2025، وهي الوثيقة التي ألغت تدفقين قديمين وجعلت PKCE إلزامياً للتطبيقات العامة. أما [OAuth 2.1](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/?ref=arabroot.io) فما زال مسودة Internet-Draft حتى كتابة هذا المقال (الإصدار 16 بتاريخ 3 سبتمبر 2026)، وهو لا يضيف بروتوكولاً جديداً، وإنما يجمع OAuth 2.0 مع ما تعلمه الجميع خلال عشر سنوات في وثيقة واحدة، فيحذف ما ثبت خطره ويجعل الممارسات الآمنة هي الافتراضية.

## OAuth تفويض وليس تسجيل دخول

أهم جملة في هذا المقال هي أن OAuth 2.0 بروتوكول للتفويض Authorization وليس لإثبات الهوية Authentication، أي أنه يجيب عن سؤال «ما الذي يسمح لهذا التطبيق أن يفعله؟» ولا يجيب عن سؤال «من هو المستخدم الذي أمامي؟». والمثال الذي يستخدمه الجميع لشرح الفكرة هو مفتاح الخادم Valet Key في بعض السيارات، فعندما تسلم سيارتك لموظف الفندق ليوقفها، تعطيه مفتاحاً خاصاً يشغل السيارة ويقودها مسافة قصيرة، ولكنه لا يفتح صندوق السيارة ولا درج القفازات، وتبقى أنت محتفظاً بالمفتاح الكامل. والتوكن في OAuth هو هذا المفتاح المحدود: يفتح أبواباً محددة لمدة محددة، ويمكن إلغاؤه دون أن تغير أقفال السيارة كلها.

وهذا الفرق ليس تفصيلاً نظرياً، لأن كثيراً من الثغرات التي ظهرت في السنوات الأولى جاءت من تطبيقات استخدمت OAuth وحده لتسجيل الدخول، كما سيأتي في قسم [OpenID Connect](#oidc).

### الأطراف الأربعة في كل عملية OAuth

تعرف [المواصفة](https://datatracker.ietf.org/doc/html/rfc6749?ref=arabroot.io#section-1.1) أربعة أدوار Roles، وكل اسم تراه في شاشات الإعداد يعود إلى واحد منها:

- **مالك المورد Resource Owner:** أنت، صاحب البيانات الذي يملك أن يوافق أو يرفض.
- **العميل Client:** التطبيق الذي يريد الوصول، مثل تطبيق طباعة الصور، أو Gitea عندما يطلب هويتك من authentik. ولاحظ أن كلمة Client هنا تعني التطبيق وليس المتصفح، فقد يكون Client سيرفراً كاملاً.
- **خادم التفويض Authorization Server:** الجهة التي تتحقق من هويتك وتسألك هل توافق، ثم تصدر التوكنات، مثل authentik وKeycloak وحسابات Google. وفي OpenID Connect يسمى هذا الطرف مزود الهوية OpenID Provider واختصاراً OP، ويسمى التطبيق الطرف المعتمد Relying Party واختصاراً RP.
- **خادم الموارد Resource Server:** الـ API الذي يحمل البيانات، ولا يقبل أي طلب إلا ومعه توكن صالح.

![مخطط الأدوار الأربعة في OAuth: مالك المورد يوافق عند خادم التفويض، وخادم التفويض يعطي التطبيق توكن وصول، والتطبيق يستدعي خادم الموارد بالتوكن، وخادم الموارد يثق بالتوكنات التي يوقعها خادم التفويض](https://arabroot.io/content/images/2026/10/oauth-oidc-02-four-roles.webp)

الأطراف الأربعة والخطوات الأربع بينها

وقد يكون خادم التفويض وخادم الموارد نظاماً واحداً، كما في Google التي تصدر التوكن وتملك Gmail في نفس الوقت، وقد يكونان منفصلين تماماً، كما في authentik أمام Gitea.

### النطاقات Scopes وشاشة الموافقة Consent

النطاق Scope كلمة يطلب بها التطبيق صلاحية محددة، مثل `photos:read` أو `repo` في GitHub، أو `openid email profile` في OpenID Connect، وكل خدمة تعرف نطاقاتها بنفسها، ولا توجد قائمة عالمية إلا نطاقات OpenID Connect. وعندما يطلب التطبيق نطاقات معينة يعرض عليك خادم التفويض شاشة الموافقة Consent Screen، وفيها اسم التطبيق وما يطلبه، فإذا وافقت حمل التوكن هذه النطاقات فقط، ولذلك يجب أن يطلب التطبيق أقل ما يحتاجه، والسبب أن التوكن قد يسرق يوماً ما، وعندها يكون الضرر بحجم النطاقات التي يحملها.

وفي تطبيقاتك الداخلية قد تختار ألا تظهر شاشة الموافقة أصلاً، كما فعلنا في دليل [ربط Gitea مع authentik عبر OIDC](https://arabroot.io/articles/%D8%B1%D8%A8%D8%B7-gitea-%D9%85%D8%B9-authentik-%D8%B9%D8%A8%D8%B1-oidc/) حين اخترنا تدفق `implicit-consent`، وهذا منطقي لأنك أنت من يملك التطبيق ومزود الهوية معاً، فلا معنى لأن يسأل الموظف كل صباح هل يسمح لـ Gitea بمعرفة بريده.

### التوكنات: الكود والـ Access Token والـ Refresh Token

في OAuth ثلاثة أشياء تتنقل بين الأطراف، ومن المهم أن تعرف الفرق بينها:

- **كود التفويض Authorization Code:** قيمة قصيرة العمر تستخدم مرة واحدة فقط، يعطيها خادم التفويض للتطبيق عبر المتصفح، ولا تفتح أي شيء بنفسها، وإنما يستبدلها التطبيق بالتوكنات. وتوصي [المواصفة](https://datatracker.ietf.org/doc/html/rfc6749?ref=arabroot.io#section-4.1.2) بألا يزيد عمرها على 10 دقائق، وأغلب المزودين يجعلونها دقيقة واحدة أو أقل.
- **توكن الوصول Access Token:** المفتاح الذي يرسله التطبيق إلى الـ API مع كل طلب، وأغلبه من نوع Bearer أي «لحامله»، ومعنى ذلك كما في [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750?ref=arabroot.io#section-1.2) أن أي أحد يحمل التوكن يستطيع استخدامه دون أن يثبت أي شيء آخر، تماماً مثل النقود في جيبك، لذلك يكون عمره قصيراً، من دقائق إلى ساعة.
- **توكن التجديد Refresh Token:** مفتاح أطول عمراً لا يذهب إلى الـ API أبداً، وإنما يرسله التطبيق إلى خادم التفويض فقط ليحصل على Access Token جديد عندما ينتهي القديم، دون أن يطلب منك تسجيل الدخول مرة أخرى.

وقد يتساءل البعض: لماذا لا يعطي خادم التفويض الـ Access Token مباشرة بدلاً من الكود؟ والإجابة في القسم التالي، لأنها مرتبطة بالطريق الذي يمر منه كل منهما.

### القناة الأمامية والقناة الخلفية

تتحرك البيانات في OAuth في طريقين مختلفين تماماً من حيث الأمان:

- **القناة الأمامية Front Channel:** كل ما يمر عبر المتصفح، أي التحويلات Redirects والروابط وشريط العنوان، وهذا الطريق مكشوف بطبيعته، فالرابط يحفظ في تاريخ المتصفح History، ويظهر في سجلات Logs الـ Reverse Proxy، وقد يرسل في ترويسة Referer إلى موقع آخر، وقد تقرؤه إضافة مثبتة في المتصفح.
- **القناة الخلفية Back Channel:** اتصال مباشر من سيرفر التطبيق إلى سيرفر خادم التفويض عبر HTTPS، لا يمر بالمتصفح ولا يراه المستخدم.

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

### التطبيق السري والتطبيق العام

تقسم المواصفة التطبيقات إلى نوعين بحسب قدرتها على حفظ سر:

- **التطبيق السري Confidential Client:** يعمل على سيرفر لا يصل إليه المستخدم، مثل Gitea وGrafana وOutline، فيستطيع أن يحفظ Client Secret في ملف إعداد أو متغير بيئة، ويثبت به هويته عند خادم التفويض.
- **التطبيق العام Public Client:** يعمل على جهاز المستخدم، مثل تطبيق جوال أو تطبيق سطح مكتب أو أداة سطر أوامر CLI أو تطبيق صفحة واحدة SPA يعمل كله في المتصفح، وأي سر تضعه في كوده يستطيع أي أحد أن يستخرجه، لذلك يعامل كأنه لا يملك سراً أصلاً.

## تدفق Authorization Code خطوة خطوة

التدفق Flow (وتسميه المواصفة Grant) هو تسلسل الطلبات الذي يحصل به التطبيق على التوكن، وتدفق Authorization Code هو التدفق الأساسي الذي تستخدمه كل تطبيقات الويب والجوال اليوم، وهو الذي يعمل خلف كل زر «الدخول بحساب…». والصورة التالية تبينه كاملاً مع PKCE، ثم نشرح كل خطوة:

![مخطط تسلسلي لتدفق Authorization Code مع PKCE بين المتصفح والتطبيق وخادم التفويض والـ API في سبع خطوات مرقمة، الخطوات عبر المتصفح باللون البنفسجي والخطوات بين السيرفرات باللون الأخضر](https://arabroot.io/content/images/2026/10/oauth-oidc-03-auth-code-pkce.webp)

سبع خطوات: ما يمر عبر المتصفح بالبنفسجي، وما يمر بين السيرفرات بالأخضر

1. تضغط «تسجيل الدخول» في التطبيق، فيولد التطبيق قيمة عشوائية طويلة اسمها `code_verifier` يحفظها عنده في الجلسة، ويحسب منها `code_challenge` وهو هاش SHA-256 لها، ويولد أيضاً قيمة عشوائية للمعامل `state`.
2. يحولك التطبيق إلى نقطة التفويض Authorization Endpoint في خادم التفويض، ومعك في الرابط: `response_type=code` و`client_id` و`redirect_uri` و`scope` و`state` و`code_challenge`.
3. تسجل الدخول في صفحة خادم التفويض نفسه، بكلمة المرور والمصادقة الثنائية، ثم توافق على النطاقات، ولاحظ أن كلمة المرور تكتب هنا فقط ولا يراها التطبيق أبداً.
4. يعيدك خادم التفويض إلى `redirect_uri` المسجل، ومعك في الرابط `code` وقيمة `state` نفسها، فيتأكد التطبيق أن `state` هي التي أرسلها.
5. يرسل التطبيق من السيرفر مباشرة طلب POST إلى نقطة التوكن Token Endpoint، ومعه الكود والـ `code_verifier` الأصلي، وسره Client Secret إذا كان تطبيقاً سرياً.
6. يحسب خادم التفويض هاش الـ `code_verifier` ويقارنه بالـ `code_challenge` الذي استلمه في الخطوة 2، ويتأكد أن الكود لم يستخدم من قبل ولم تنته صلاحيته، ثم يرد بالـ Access Token ومعه Refresh Token إذا طلبه التطبيق، وID Token إذا كان الطلب OpenID Connect.
7. يستدعي التطبيق الـ API ومعه الترويسة `Authorization: Bearer` والتوكن.

والآن نعود إلى السؤال السابق: لماذا كل هذا الدوران بدلاً من أن يرسل خادم التفويض التوكن في الخطوة 4 مباشرة؟ والسبب أن الخطوة 4 تمر في القناة الأمامية، فالرابط الذي يحمل الكود يظهر في شريط العنوان وفي السجلات، ولو كان يحمل التوكن نفسه لكان التوكن مكشوفاً في كل هذه الأماكن، أما الكود فلا يفيد من يراه لأنه يحتاج إلى الـ `code_verifier` والسر، وهما لم يغادرا السيرفر.

### لماذا أصبح PKCE إلزامياً؟

لنفترض أن مخترقاً استطاع أن يرى الكود في الخطوة 4، سواء من سجلات الـ Reverse Proxy، أو من تطبيق خبيث على الجوال سجل نفس رابط العودة Custom URL Scheme الذي يستخدمه تطبيقك، وهي [المشكلة التي صمم من أجلها PKCE](https://datatracker.ietf.org/doc/html/rfc7636?ref=arabroot.io#section-1) في البداية. فما الذي يمنعه أن يستبدل الكود بالتوكنات بنفسه؟

- **الحل الأول: سر التطبيق Client Secret.** فالمخترق لا يملك السر، وبالتالي لا يستطيع أن يستبدل الكود. وهذا يعمل مع التطبيقات السرية، ولكنه لا يعمل مع تطبيق الجوال أو الـ SPA، لأن السر داخل الكود الذي يستطيع أي أحد أن يستخرجه، فهو ليس سراً أصلاً.
- **الحل الصحيح: إثبات أن من بدأ الطلب هو من يكمله.** وهذه فكرة PKCE (تنطق «بيكسي»)، واسمها الكامل Proof Key for Code Exchange، حيث يولد التطبيق قيمة عشوائية جديدة لكل عملية دخول هي الـ `code_verifier`، ويرسل في البداية هاشها فقط وهو الـ `code_challenge`، ثم يرسل القيمة الأصلية في النهاية. والمخترق الذي رأى الرابط الأول رأى الهاش فقط، ولا يستطيع أن يعرف القيمة الأصلية منه، لأن الهاش دالة باتجاه واحد One-Way Function، والذي رأى الكود في الخطوة 4 لا يملك القيمة الأصلية أيضاً، وبالتالي لا يستطيع أي منهما أن يكمل الطلب.

وتعرف المواصفة طريقتين لحساب الـ `code_challenge`: الطريقة `S256` وهي الهاش، والطريقة `plain` التي ترسل القيمة نفسها دون هاش، وهذه لا ينصح باستخدامها إطلاقاً، والسبب أن من يرى الطلب الأول يرى القيمة كاملة، لذلك ينص RFC 9700 على أن `S256` هي الطريقة الوحيدة الآمنة حالياً، وحذفتها مسودة OAuth 2.1 من المواصفة تماماً.

وبدأ PKCE كحل لتطبيقات الجوال، ولكن [RFC 9700 في القسم 2.1.1](https://datatracker.ietf.org/doc/html/rfc9700?ref=arabroot.io#section-2.1.1) جعله إلزامياً MUST لكل تطبيق عام، وموصى به RECOMMENDED للتطبيقات السرية أيضاً، والسبب أنه يحمي من هجوم حقن الكود Authorization Code Injection، حيث يضع المخترق كوداً مسروقاً في جلسة ضحية أخرى، وهذا الهجوم لا يمنعه سر التطبيق. وذهبت [مسودة OAuth 2.1](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/?ref=arabroot.io) خطوة أبعد، فجعلت استخدام `code_challenge` و`code_verifier` واجباً على كل التطبيقات إلا في حالة واحدة، وهي التطبيق السري في OpenID Connect الذي يستخدم المعامل `nonce` بشكل صحيح، ومع ذلك تبقى توصيتها باستخدام PKCE في هذه الحالة أيضاً. والخلاصة العملية بسيطة: فعل PKCE في كل تطبيق تربطه بمزود الهوية، فلا يوجد سبب لتركه.

### المعامل state وهجمات CSRF

هناك هجوم آخر يعمل في الاتجاه المعاكس، فبدلاً من أن يسرق المخترق كودك، يجعلك أنت تستخدم كوده. يبدأ المخترق عملية دخول بحسابه، ويوقفها عند الخطوة 4 ويأخذ الكود، ثم يرسل لك رابطاً مثل `https://app.example.com/callback?code=كود_المخترق`، فإذا فتحته يدخل التطبيق باسمك إلى حساب المخترق، وكل ما ترفعه بعد ذلك من ملفات أو بيانات بطاقات يذهب إلى حسابه. وهذا نوع من تزوير الطلبات عبر المواقع Cross-Site Request Forgery واختصاراً CSRF، ويشرحه [القسم 10.12 من RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749?ref=arabroot.io#section-10.12).

والحل أن يولد التطبيق في الخطوة 1 قيمة عشوائية لا تتكرر في المعامل `state`، ويربطها بجلسة المتصفح، ثم يرفض أي رد في الخطوة 4 لا يحمل نفس القيمة، والرابط الذي أرسله المخترق لا يحمل القيمة المحفوظة في جلستك، وبالتالي يرفضه التطبيق. ويجدر الإشارة هنا إلى أن [RFC 9700](https://datatracker.ietf.org/doc/html/rfc9700?ref=arabroot.io#section-2.1) يسمح للتطبيق أن يعتمد على PKCE بدلاً من `state` في الحماية من CSRF إذا تأكد أن مزود الهوية يدعمه، وفي OpenID Connect يقوم المعامل `nonce` بنفس الدور، ولكن أغلب المكتبات ترسل الثلاثة معاً، ولا يوجد ضرر في ذلك.

### لماذا يجب أن يطابق redirect\_uri حرفاً بحرف؟

عنوان العودة Redirect URI هو المكان الذي يرسل إليه خادم التفويض الكود في الخطوة 4، وإذا استطاع المخترق أن يغيره إلى عنوان يملكه فإنه يستلم الكود مباشرة، لذلك يسجل كل تطبيق عناوين العودة عند مزود الهوية مسبقاً، ويرفض المزود أي طلب لا يطابق عنواناً مسجلاً. والمهم هنا هو كلمة «يطابق»، فقد كانت بعض المزودات تقبل أي عنوان يبدأ بالعنوان المسجل، أو تقبل نطاقاً فرعياً Subdomain بنجمة Wildcard، وظهرت هجمات كثيرة تستغل ذلك عبر صفحة تحويل مفتوحة Open Redirect في الموقع نفسه، لذلك ينص [RFC 9700](https://datatracker.ietf.org/doc/html/rfc9700?ref=arabroot.io#section-2.1) على المقارنة الحرفية Exact String Matching، مع استثناء واحد هو رقم المنفذ في عناوين `localhost` لتطبيقات سطح المكتب.

ومعنى ذلك أن الفرق بين `http` و`https`، أو منفذ زائد مثل `:3000`، أو شرطة مائلة في الآخر، يكفي لرفض الطلب، وهذا أكثر خطأ سوف تقابله عندما يكون تطبيقك خلف Reverse Proxy، لأن التطبيق قد يبني عنوان العودة من عنوانه الداخلي، وقد شرحنا سببه وحله في قسم [روابط العودة في OAuth وOIDC](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/#oauth) من دليل التطبيق خلف Reverse Proxy.

## بقية التدفقات: متى تستخدم كل واحد؟

تدفق Authorization Code يفترض وجود مستخدم ومتصفح، ولكن هناك حالتان لا يتوفر فيهما ذلك: خدمة تعمل وحدها دون مستخدم، وجهاز فيه مستخدم ولكن ليس فيه متصفح أو لوحة مفاتيح مريحة، والصورة التالية تبين التدفقين:

![مخطط من جزأين: يساراً خدمة نسخ احتياطي تطلب توكن من خادم التفويض بمعرفها وسرها ثم تستدعي API المراقبة، ويميناً تلفاز يطلب كوداً ويعرضه فيوافق المستخدم من جواله بينما يسأل التلفاز نقطة التوكن كل خمس ثوان](https://arabroot.io/content/images/2026/10/oauth-oidc-04-other-flows.webp)

Client Credentials لتطبيق يعمل باسمه، وDevice Flow لجهاز دون متصفح

### Client Credentials: تطبيق يتصل بتطبيق

عندما تريد مهمة نسخ احتياطي أو خدمة مراقبة أن تستدعي API لا يوجد مستخدم يوافق، والتطبيق يعمل باسمه هو، فيرسل معرفه وسره مباشرة إلى نقطة التوكن مع `grant_type=client_credentials`، ويحصل على Access Token دون Refresh Token، لأنه يستطيع أن يطلب توكن جديداً متى شاء. وهذا التدفق يحل محل ما يفعله كثيرون حين يضعون مفتاح API ثابتاً لا تنتهي صلاحيته في ملف إعداد، فالتوكن هنا قصير العمر ومحدود بنطاقات، والمفتاح الطويل (السر) لا يرسل إلا إلى خادم التفويض. ويدعمه authentik كما في [توثيق مزود OAuth2](https://docs.goauthentik.io/add-secure-apps/providers/oauth2/?ref=arabroot.io)، والطلب يكون بالشكل التالي:

```bash
curl -s -u backup-job:"$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d scope=monitoring:read \
  https://auth.example.com/application/o/token/
```

ولاحظ أن الطلب لا يحمل اسم مستخدم ولا كلمة مرور لأحد، وأن التوكن الذي يعود منه لا يمثل أي شخص، لذلك لا تستخدمه لتسجيل دخول مستخدم أبداً.

### Device Authorization: تلفاز أو أداة سطر أوامر

لنفترض أنك تريد تسجيل الدخول في تطبيق على التلفاز، أو في أداة سطر أوامر على سيرفر تتصل به عبر SSH، فكتابة البريد وكلمة المرور بجهاز التحكم عملية مزعجة، والأداة على السيرفر لا تملك متصفحاً أصلاً. والحل في [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628?ref=arabroot.io) أن يطلب الجهاز من خادم التفويض كوداً قصيراً للمستخدم User Code ورابطاً، فيعرضهما على الشاشة، ثم تفتح الرابط من جوالك أو حاسوبك وتكتب الكود وتسجل الدخول هناك بكل وسائلك المعتادة، وفي الأثناء يسأل الجهاز نقطة التوكن كل بضع ثوان: هل وافق المستخدم؟ فيرد خادم التفويض بالخطأ `authorization_pending` حتى توافق، ثم يرد بالتوكنات. وهذا ما تفعله أدوات مثل GitHub CLI وAzure CLI عندما تطلب منك فتح رابط وكتابة كود، وسوف نجربه بأنفسنا في القسم العملي.

### تدفق Refresh Token

عندما تنتهي صلاحية الـ Access Token يرسل التطبيق الـ Refresh Token إلى نقطة التوكن مع `grant_type=refresh_token`، فيحصل على Access Token جديد دون أن يرى المستخدم أي شيء. ولاحظ أن authentik منذ الإصدار 2024.2 لا يصدر Refresh Token إلا إذا طلب التطبيق النطاق `offline_access`، وهذا ما ذكرناه في دليل Gitea عن مهمة مزامنة المستخدمين.

### تدفقان انتهى زمنهما: Implicit وPassword

في OAuth 2.0 الأصلي تدفقان لا ينصح باستخدامهما اليوم، وسوف تجدهما في تطبيقات قديمة وشروحات قديمة، لذلك من المهم أن تعرف لماذا ألغيا:

- **التدفق الضمني Implicit Grant:** صمم في 2012 لتطبيقات المتصفح التي لم تكن تستطيع إرسال طلبات إلى نطاق آخر قبل انتشار CORS، فكان خادم التفويض يرسل الـ Access Token مباشرة في رابط العودة (`response_type=token`)، أي في القناة الأمامية، فيظهر التوكن في تاريخ المتصفح والسجلات وقد يتسرب عبر Referer، ولا توجد طريقة لربطه بالتطبيق الذي طلبه. لذلك ينص [RFC 9700](https://datatracker.ietf.org/doc/html/rfc9700?ref=arabroot.io#section-2.1.2) على ألا تستخدمه التطبيقات SHOULD NOT، وحذفته مسودة OAuth 2.1 تماماً، والبديل لتطبيقات المتصفح هو Authorization Code مع PKCE.
- **تدفق كلمة المرور Resource Owner Password Credentials:** يأخذ التطبيق اسم المستخدم وكلمة المرور ويرسلهما إلى نقطة التوكن، أي أنه يعيدنا إلى نمط مشاركة كلمة المرور الذي جاء OAuth ليلغيه، ولا يعمل مع المصادقة الثنائية ولا مع مفاتيح المرور Passkeys، ويعود المستخدمين على كتابة كلمة مرورهم في أي مكان يطلبها. لذلك ينص [RFC 9700](https://datatracker.ietf.org/doc/html/rfc9700?ref=arabroot.io#section-2.4) على أنه MUST NOT be used، أي لا يستخدم إطلاقاً.

## OpenID Connect: من يكون المستخدم؟

بعد انتشار OAuth 2.0 بدأت تطبيقات كثيرة تستخدمه لتسجيل الدخول بطريقة تبدو منطقية: يحصل التطبيق على Access Token، ثم يستدعي API مثل `/me` ليعرف من صاحب الحساب، فيسجل دخوله. والمشكلة أن الـ Access Token صمم ليقول للـ API «هذا التطبيق مسموح له أن يقرأ»، ولم يصمم ليقول للتطبيق «هذا المستخدم دخل إليك الآن»، فهو لا يحمل اسم التطبيق الذي صدر من أجله، ولا وقت الدخول، ولا أي رابط بالجلسة التي بدأت في متصفحك. وبالتالي إذا حصل مخترق على Access Token صادر لتطبيق آخر يملكه هو، لمستخدم دخل إلى تطبيقه يوماً ما، يستطيع أن يقدمه لتطبيقك فيعتقد تطبيقك أن هذا المستخدم هو من سجل الدخول، وهذا ما يشرحه مقال [End User Authentication with OAuth 2.0](https://oauth.net/articles/authentication/?ref=arabroot.io) في موقع oauth.net بالتفصيل.

والحل الذي جاء به OpenID Connect أن يضيف إلى OAuth 2.0 شيئين: النطاق `openid` الذي يطلب به التطبيق إثبات الهوية، وتوكن جديد اسمه **ID Token** موجه للتطبيق نفسه وموقع من مزود الهوية، ويقول بوضوح: هذا المستخدم، دخل في هذا الوقت، وهذا التوكن صادر لهذا التطبيق بالذات. ولاحظ أن OpenID Connect لا يغير شيئاً في التدفق الذي شرحناه، فهو نفس Authorization Code مع PKCE، والفرق أن الطلب يحمل `scope=openid` والرد يحمل `id_token` بجانب الـ Access Token.

### الفرق بين الـ ID Token والـ Access Token

الخلط بين التوكنين هو مصدر أغلب الأخطاء في هذا المجال، والصورة التالية تبين لمن يذهب كل منهما:

![مخطط من جزأين: يميناً الـ ID Token ينتقل من مزود الهوية إلى التطبيق ويحمل iss وsub وaud باسم التطبيق، ويساراً الـ Access Token يحمله التطبيق إلى الـ API ويحمل النطاق وaud باسم الـ API](https://arabroot.io/content/images/2026/10/oauth-oidc-05-id-vs-access.webp)

الـ ID Token يقرؤه التطبيق، والـ Access Token يقرؤه الـ API

- **الـ ID Token للتطبيق:** يقرؤه التطبيق ويتحقق منه مرة واحدة عند الدخول، ثم ينشئ جلسته الخاصة، والحقل `aud` فيه هو معرف التطبيق Client ID. فلا تقم بإرساله إلى أي API كأنه مفتاح، والسبب أنه لم يصدر من أجل الـ API ولا يحمل أي صلاحيات.
- **الـ Access Token للـ API:** التطبيق يحمله فقط ولا يجب أن يعتمد على محتواه، وقد يكون JWT يستطيع الـ API قراءته، وقد يكون قيمة عشوائية لا معنى لها إلا عند مزود الهوية، فلا تقم باستخدامه كدليل على أن المستخدم سجل الدخول إلى تطبيقك.

### كيف تقرأ JWT؟

الـ ID Token دائماً بصيغة JSON Web Token واختصاراً JWT (تنطق «جوت»)، وهي معرفة في [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519?ref=arabroot.io)، وتتكون من ثلاثة أجزاء بينها نقطتان: `header.payload.signature`، وكل جزء مرمز بترميز Base64url وهو نفس Base64 مع استبدال الرمزين `+` و`/` بالرمزين `-` و`_` وحذف علامات `=` في الآخر، حتى يصلح التوكن داخل الروابط. وقد شرحنا فكرة JWT من الأساس، والفرق بينه وبين الجلسة على السيرفر وملفات تعريف الارتباط، في مقال [الجلسات وملفات تعريف الارتباط وJWT للمبتدئين](https://arabroot.io/articles/%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D9%88%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D8%AA%D8%B9%D8%B1%D9%8A%D9%81-%D8%A7%D9%84%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-%D9%88-jwt-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، وهنا نركز على ما يخص OpenID Connect. والصورة التالية تبين الأجزاء الثلاثة لتوكن حقيقي أصدرناه في التجربة العملية:

![مخطط يفكك توكن JWT حقيقياً إلى الترويسة وفيها alg وkid، والمحتوى وفيه iss وsub وaud وexp وiat وnonce وemail، والتوقيع المحسوب بالمفتاح الخاص لمزود الهوية، ثم التحقق بالمفتاح العام من JWKS حيث يصح التوكن الأصلي ويفشل التوكن الذي غير فيه البريد](https://arabroot.io/content/images/2026/10/oauth-oidc-06-jwt-anatomy.webp)

ثلاثة أجزاء: الترويسة والمحتوى يقرؤهما أي أحد، والتوقيع يكشف أي تعديل

- **الترويسة Header:** تقول كيف وقع التوكن، فالحقل `alg` هو خوارزمية التوقيع مثل `RS256`، والحقل `kid` هو معرف المفتاح الذي استخدم، حتى يعرف التطبيق أي مفتاح عام يستخدم في التحقق.
- **المحتوى Payload:** الحقول Claims التي يقولها التوكن عن المستخدم، وسوف نشرحها بعد قليل.
- **التوقيع Signature:** يأخذ مزود الهوية الجزأين الأولين كما هما مع النقطة بينهما، ويوقعهما بمفتاحه الخاص Private Key، أي أنه يحسب هاش النص ثم يشفر الهاش بالمفتاح الخاص، وأي تطبيق يملك المفتاح العام Public Key يستطيع أن يحسب الهاش من جديد ويقارنه بما في التوقيع.

وهنا أهم ملاحظة في هذا القسم: Base64url ترميز Encoding وليس تشفيراً Encryption، فأي أحد يحمل التوكن يستطيع أن يقرأ محتواه كما سوف نفعل في التجربة العملية بأمر واحد، لذلك لا تقم بوضع أي معلومة سرية في محتوى JWT. والتوقيع لا يخفي المحتوى، وإنما يضمن شيئين: أن من أصدره هو صاحب المفتاح الخاص، وأن أحداً لم يغير حرفاً واحداً بعد ذلك، فإذا غير المخترق البريد في المحتوى من `alice` إلى `admin` فإن الهاش يتغير، ولا يستطيع أن يحسب توقيعاً جديداً لأنه لا يملك المفتاح الخاص، وبالتالي يفشل التحقق.

وقد يتساءل البعض: لماذا لا يوقع مزود الهوية بسر مشترك بدلاً من زوج المفاتيح؟ والإجابة أن هذا ممكن بالخوارزمية `HS256`، وهي HMAC بسر واحد يعرفه المزود والتطبيق، ولكن كل من يستطيع التحقق يستطيع التوقيع أيضاً، فإذا كان لديك عشرة تطبيقات تعرف السر فأي واحد منها يستطيع أن يصنع توكن باسم أي مستخدم، أما مع `RS256` أو `ES256` فالمفتاح الخاص لا يغادر مزود الهوية، والتطبيقات تملك المفتاح العام فقط، لذلك اخترنا في دليل Gitea مفتاح توقيع Signing Key في authentik بدلاً من تركه فارغاً.

### الحقول القياسية Claims

تعرف [مواصفة OpenID Connect Core](https://openid.net/specs/openid-connect-core-1%5F0.html?ref=arabroot.io#IDToken) مجموعة حقول في الـ ID Token، وأهمها:

| الحقل                        | معناه                                                 | ما الذي يفعله التطبيق به                                                        |
| ---------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------- |
| iss                          | المصدر Issuer: عنوان مزود الهوية                      | يجب أن يطابق عنوان المزود الذي يثق به التطبيق حرفاً بحرف                        |
| sub                          | الموضوع Subject: معرف المستخدم الثابت داخل هذا المزود | هو المفتاح الذي يربط به التطبيق حساب المستخدم، وليس البريد، لأن البريد قد يتغير |
| aud                          | الجمهور Audience: لمن صدر التوكن                      | يجب أن يحتوي Client ID الخاص بالتطبيق، وإلا فالتوكن صدر لتطبيق آخر              |
| exp                          | وقت انتهاء الصلاحية Expiration                        | يرفض التوكن بعده، بفارق صغير لاختلاف الساعات Clock Skew                         |
| iat                          | وقت الإصدار Issued At                                 | يرفض التوكن الذي صدر في وقت بعيد جداً                                           |
| nonce                        | قيمة عشوائية أرسلها التطبيق في طلب الدخول             | يجب أن تطابق القيمة المحفوظة في الجلسة، فتمنع إعادة استخدام توكن قديم Replay    |
| email وemail\_verified وname | معلومات الملف الشخصي، حسب النطاقات email وprofile     | للعرض وللبحث عن الحساب أول مرة، مع التأكد أن email\_verified صحيحة              |

### نقطة UserInfo

بجانب الـ ID Token يوفر مزود الهوية نقطة اسمها UserInfo Endpoint، يستدعيها التطبيق بالـ Access Token فترد بمعلومات المستخدم في JSON، وهي مفيدة عندما يكون الـ ID Token صغيراً ولا يحمل كل الحقول، أو عندما يريد التطبيق معلومات محدثة بعد الدخول. وبعض التطبيقات مثل Gitea تعتمد عليها لجلب الاسم والبريد والمجموعات Groups.

### الاكتشاف Discovery: عنوان واحد يكفي

حتى يعمل التطبيق مع مزود الهوية يحتاج أن يعرف عناوين كثيرة: نقطة التفويض، ونقطة التوكن، ونقطة UserInfo، ومكان المفاتيح العامة، والخوارزميات المدعومة، وبدلاً من أن تنسخها واحداً واحداً تعرف [مواصفة OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1%5F0.html?ref=arabroot.io) عنواناً ثابتاً تحت عنوان المزود هو `/.well-known/openid-configuration`، يرد بملف JSON فيه كل شيء، ولذلك تكتفي أغلب التطبيقات بحقل واحد اسمه Issuer URL أو Discovery URL. والمثال التالي يطلب هذا الملف من Google:

```bash
curl -s https://accounts.google.com/.well-known/openid-configuration
```

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

```json
{
 "issuer": "https://accounts.google.com",
 "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
 "device_authorization_endpoint": "https://oauth2.googleapis.com/device/code",
 "token_endpoint": "https://oauth2.googleapis.com/token",
 "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
 "revocation_endpoint": "https://oauth2.googleapis.com/revoke",
 "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
 "response_types_supported": [
  "code",
  "token",
  "id_token",
  "code token",
  "code id_token",
  "token id_token",
  "code token id_token",
  "none"
 ],
 "response_modes_supported": [
  "query",
  "fragment",
  "form_post"
 ],
 "subject_types_supported": [
  "public"
 ],
 "id_token_signing_alg_values_supported": [
  "RS256"
 ],
 "scopes_supported": [
  "openid",
  "email",
  "profile"
 ],
 "token_endpoint_auth_methods_supported": [
  "client_secret_post",
  "client_secret_basic"
 ],
 "claims_supported": [
  "aud",
  "email",
  "email_verified",
  "exp",
  "family_name",
  "given_name",
  "iat",
  "iss",
  "name",
  "picture",
  "sub"
 ],
 "code_challenge_methods_supported": [
  "plain",
  "S256"
 ],
 "grant_types_supported": [
  "authorization_code",
  "refresh_token",
  "urn:ietf:params:oauth:grant-type:device_code",
  "urn:ietf:params:oauth:grant-type:jwt-bearer"
 ],
 "authorization_response_iss_parameter_supported": true
}
```

في المخرج أعلاه لاحظ التالي:

- القيمة `issuer` هي التي يجب أن تجدها حرفياً في الحقل `iss` لكل ID Token يصدره المزود، وهذا ما يفحصه Gitea حين يظهر الخطأ «issuer in token does not match» الذي شرحناه في [دليل Gitea](https://arabroot.io/articles/%D8%B1%D8%A8%D8%B7-gitea-%D9%85%D8%B9-authentik-%D8%B9%D8%A8%D8%B1-oidc/#t-issuer).
- القيمة `jwks_uri` هي عنوان المفاتيح العامة التي يتحقق بها التطبيق من التوقيع.
- القائمة `code_challenge_methods_supported` تبين أن المزود يدعم PKCE، والقائمة `grant_types_supported` تبين التدفقات المسموحة، وهكذا تعرف ما يدعمه أي مزود قبل أن تكتب سطراً واحداً.
- ما زالت Google تعلن `token` ضمن `response_types_supported`، أي أن التدفق الضمني ما زال متاحاً عندها للتطبيقات القديمة، فلا تقم باستخدامه في أي تطبيق جديد، والسبب في قسم [التدفقات التي انتهى زمنها](#deprecated).

### التحقق من التوقيع عبر JWKS

الملف الذي يشير إليه `jwks_uri` بصيغة JSON Web Key Set واختصاراً JWKS، المعرفة في [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517?ref=arabroot.io)، وفيه قائمة بالمفاتيح العامة لمزود الهوية، ولكل مفتاح معرف `kid`. وعندما يستلم التطبيق ID Token يقرأ `kid` من الترويسة، ويختار المفتاح المطابق من القائمة، ثم يتحقق من التوقيع، وبعد ذلك يفحص `iss` و`aud` و`exp` و`nonce` كما في [قسم التحقق من الـ ID Token](https://openid.net/specs/openid-connect-core-1%5F0.html?ref=arabroot.io#IDTokenValidation) في المواصفة. ووجود عدة مفاتيح في القائمة له سبب، فمزود الهوية يغير مفاتيحه دورياً Key Rotation، فينشر المفتاح الجديد قبل أن يستخدمه ويبقي القديم مدة حتى تنتهي التوكنات الموقعة به، لذلك لا تقم بنسخ المفتاح العام إلى إعداد التطبيق يدوياً، وإنما اجعله يقرأ JWKS ويحفظها مدة قصيرة Cache.

💡

تسمح المواصفة للتطبيق السري أن يكتفي بالتحقق من اتصال TLS بدلاً من التوقيع، إذا استلم الـ ID Token مباشرة من نقطة التوكن في القناة الخلفية، وهذا ما يفعله Gitea كما ذكرنا في دليله، ولكن أي مكتبة OIDC حديثة تتحقق من التوقيع على أي حال، فلا تقم بتعطيل ذلك في إعداد تطبيقك.

## كيف يبدو هذا في تطبيقاتك المستضافة ذاتياً؟

الآن نضع ما سبق على سيرفرك: authentik أو Keycloak أو أي مزود هوية آخر هو خادم التفويض ومزود الهوية في نفس الوقت، وكل تطبيق تربطه به (Gitea وOutline وGrafana وغيرها) هو Client من النوع السري، يستخدم تدفق Authorization Code ومعه OpenID Connect ليعرف من المستخدم، ثم ينشئ جلسته الخاصة. والذي يجعلك لا تكتب كلمة المرور إلا مرة واحدة هو جلسة مزود الهوية نفسه، فعندما يحولك التطبيق الثاني إلى authentik يجد authentik أنك سجلت الدخول قبل ساعة، فيعيدك مباشرة بكود جديد دون أي سؤال، وهذا هو الدخول الموحد Single Sign-On واختصاراً SSO:

![مخطط يبين مستخدماً يفتح Gitea الساعة التاسعة فيسجل الدخول في authentik بكلمة المرور والمصادقة الثنائية مرة واحدة، ثم يفتح Outline وGrafana لاحقاً فيعيده authentik دون كلمة مرور لأن جلسته ما زالت قائمة، ولكل تطبيق جلسته الخاصة](https://arabroot.io/content/images/2026/10/oauth-oidc-07-sso-apps.webp)

جلسة واحدة في مزود الهوية، وجلسة مستقلة في كل تطبيق

ولهذا التصميم فائدة أكبر من راحة المستخدم، فعندما يغادر موظف تعطل حسابه في مكان واحد، فلا يستطيع أن يسجل الدخول إلى أي تطبيق بعد ذلك، ولكن تذكر أن الجلسات القائمة في التطبيقات والتوكنات التي أصدرها كل تطبيق بنفسه تبقى حتى تنتهي، كما شرحنا في [الملاحظات الأمنية في دليل Gitea](https://arabroot.io/articles/%D8%B1%D8%A8%D8%B7-gitea-%D9%85%D8%B9-authentik-%D8%B9%D8%A8%D8%B1-oidc/#security). أما إنشاء الحسابات وحذفها في التطبيقات تلقائياً عند إضافة الموظف أو مغادرته فهذا موضوع بروتوكول آخر شرحناه في [ما هو 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/).

وفي دليل [تسجيل الدخول بحساب Google في authentik](https://arabroot.io/articles/%D8%AA%D8%B3%D8%AC%D9%8A%D9%84-%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A8%D8%AD%D8%B3%D8%A7%D8%A8-google-%D9%81%D9%8A-authentik/) تنعكس الأدوار، فيصبح authentik نفسه Client عند Google، وGoogle هي مزود الهوية، ثم يصبح authentik مزود الهوية لتطبيقاتك، أي أن نفس البروتوكول يعمل في السلسلة كلها. وإذا كان لديك تطبيق لا يدعم OIDC أصلاً، فلا يزال بإمكانك أن تضعه خلف authentik عبر الـ Reverse Proxy كما في دليل [حماية أي تطبيق باستخدام authentik Forward Auth](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D8%A3%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-authentik-forward-auth/)، وهناك يقوم الـ outpost بدور الـ Client نيابة عن التطبيق.

### ماذا تعني الحقول في شاشة الإعداد؟

كل تطبيق يسمي الحقول بطريقته، ولكنها تعود إلى نفس المفاهيم، والجدول التالي يجمعها:

| الحقل                              | معناه                                                            | أين تجده                                                                                                                                                                                             |
| ---------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Client ID                          | اسم التطبيق عند مزود الهوية، وليس سراً، ويظهر في الروابط وفي aud | ينشئه المزود، وتنسخه إلى التطبيق                                                                                                                                                                     |
| Client Secret                      | كلمة مرور التطبيق نفسه، يرسلها في القناة الخلفية فقط             | ينشئه المزود، ويحفظ في ملف .env أو متغير بيئة، ولا يكتب في Git أبداً                                                                                                                                 |
| Client Type: Confidential / Public | هل يستطيع التطبيق حفظ سر؟                                        | Confidential لكل تطبيق ويب يعمل على سيرفر                                                                                                                                                            |
| Redirect URI / Callback URL        | العنوان الذي يعود إليه الكود، ويقارن حرفاً بحرف                  | مسار ثابت في كل تطبيق، مثل /user/oauth2/authentik/callback في Gitea و/auth/oidc.callback في Outline و/login/generic\_oauth في Grafana                                                                |
| Scopes                             | ما يطلبه التطبيق، وأقلها openid email profile                    | في التطبيق، ويجب أن يسمح بها المزود                                                                                                                                                                  |
| Issuer / Discovery URL             | عنوان المزود الذي يجلب منه التطبيق كل العناوين الأخرى            | صفحة المزود في authentik، مثل https://auth.example.com/application/o/gitea/                                                                                                                          |
| Signing Key                        | المفتاح الذي يوقع به المزود الـ ID Token                         | في المزود، واختر مفتاحاً غير متماثل حتى يكون التوقيع RS256                                                                                                                                           |
| PKCE                               | تفعيل code\_challenge                                            | خيار في التطبيق، مثل use\_pkce في [إعداد Generic OAuth في Grafana](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/generic-oauth/?ref=arabroot.io) |

وقد شرحنا تفاصيل هذه الحقول في authentik وGitea خطوة خطوة في دليل [ربط Gitea مع authentik عبر OIDC](https://arabroot.io/articles/%D8%B1%D8%A8%D8%B7-gitea-%D9%85%D8%B9-authentik-%D8%B9%D8%A8%D8%B1-oidc/)، ومع Outline في [دليل تثبيت Outline](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-outline-%D9%84%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D9%85%D8%B9%D8%B1%D9%81%D8%A9-%D8%A7%D9%84%D9%81%D8%B1%D9%8A%D9%82/).

## تجربة عملية: نمشي التدفق بأنفسنا مع Dex وcurl

كل ما سبق يبقى نظرياً حتى ترى الطلبات والردود بعينك، لذلك سوف نشغل مزود هوية صغيراً على جهازك، ونقوم بدور التطبيق بأنفسنا باستخدام curl، فنولد قيم PKCE ونبني رابط التفويض ونستبدل الكود ونقرأ التوكن ونتحقق من توقيعه. ولم نستخدم authentik هنا لأنه يحتاج قاعدة بيانات وعدة Containers، واخترنا [Dex](https://dexidp.io/?ref=arabroot.io)، وهو مزود OpenID Connect مفتوح المصدر يعمل في Container واحد بملف إعداد واحد، والإصدار المستخدم هو [v2.45.1](https://github.com/dexidp/dex/releases/tag/v2.45.1?ref=arabroot.io). وتحتاج على جهازك Docker مع Compose، وcurl وopenssl، وNode.js لخطوة التحقق من التوقيع فقط.

### تشغيل مزود الهوية

أنشئ مجلداً باسم `lab-oidc` وضع فيه الملف `compose.yaml` التالي، ولاحظ أن المنفذ مربوط بالعنوان `127.0.0.1` فقط حتى لا يصل إليه أحد من الشبكة:

```yaml
name: lab-oidc
services:
  dex:
    image: ghcr.io/dexidp/dex:v2.45.1
    container_name: lab-oidc-dex
    command: ["dex", "serve", "/etc/dex/config.yaml"]
    volumes:
      - ./dex.yaml:/etc/dex/config.yaml:ro
    ports:
      - "127.0.0.1:18556:5556"
```

ثم ملف إعداد Dex باسم `dex.yaml` في نفس المجلد:

```yaml
issuer: http://127.0.0.1:18556/dex
storage:
  type: memory
web:
  http: 0.0.0.0:5556
expiry:
  idTokens: "1h"
  refreshTokens:
    disableRotation: false
oauth2:
  responseTypes: ["code"]
  skipApprovalScreen: true
staticClients:
  - id: notes-app
    name: Notes App
    secret: lab-only-secret-change-me
    redirectURIs:
      - http://127.0.0.1:18080/callback
  - id: cli-tool
    name: CLI Tool
    public: true
    redirectURIs:
      - /device/callback
enablePasswordDB: true
staticPasswords:
  - email: alice@example.com
    hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
    username: alice
    name: Alice
    userID: 08a8684b-db88-4b73-90a9-3cd1661f5466
```

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

- `issuer` هو العنوان الذي سوف يظهر في الحقل `iss` لكل توكن، وتحته عنوان الاكتشاف.
- `storage: memory` يعني أن كل شيء يضيع عند إعادة التشغيل، وهذا مناسب للتجربة فقط.
- `responseTypes: ["code"]` يسمح بتدفق Authorization Code وحده، فلا يوجد تدفق ضمني.
- `skipApprovalScreen: true` يلغي شاشة الموافقة، كما نفعل مع التطبيقات الداخلية.
- التطبيق `notes-app` تطبيق سري له سر وعنوان عودة واحد، والتطبيق `cli-tool` تطبيق عام دون سر، سوف نستخدمه في تدفق الأجهزة.
- المستخدم `alice@example.com` وكلمة مروره `password`، والهاش هو المثال الموجود في توثيق Dex نفسه، والسر `lab-only-secret-change-me` للتجربة فقط، فلا تقم باستخدام أي منهما خارج جهازك.

الآن نقوم بتشغيله ونقرأ السجل:

```bash
docker compose up -d
docker logs lab-oidc-dex 2>&1 | tail -4
```

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

```text
time=2026-10-05T20:00:55.662Z level=INFO msg="config refresh tokens rotation" enabled=true
time=2026-10-05T20:00:55.663Z level=INFO msg="keys expired, rotating"
time=2026-10-05T20:00:55.701Z level=INFO msg="keys rotated" next_rotation=2026-10-06T02:00:55.701Z
time=2026-10-05T20:00:55.701Z level=INFO msg="listening on" server=http address=0.0.0.0:5556
```

ولاحظ السطرين الأوسطين، فقد ولد Dex زوج مفاتيح التوقيع عند التشغيل، وسوف يغيره كل 6 ساعات، وهذا هو تغيير المفاتيح الدوري الذي ذكرناه في قسم JWKS. والآن نطلب ملف الاكتشاف كما فعلنا مع Google:

```bash
curl -s http://127.0.0.1:18556/dex/.well-known/openid-configuration
```

```json
{
  "issuer": "http://127.0.0.1:18556/dex",
  "authorization_endpoint": "http://127.0.0.1:18556/dex/auth",
  "token_endpoint": "http://127.0.0.1:18556/dex/token",
  "jwks_uri": "http://127.0.0.1:18556/dex/keys",
  "userinfo_endpoint": "http://127.0.0.1:18556/dex/userinfo",
  "device_authorization_endpoint": "http://127.0.0.1:18556/dex/device/code",
  "introspection_endpoint": "http://127.0.0.1:18556/dex/token/introspect",
  "grant_types_supported": [
    "authorization_code",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:device_code",
    "urn:ietf:params:oauth:grant-type:token-exchange"
  ],
  "response_types_supported": [
    "code"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "code_challenge_methods_supported": [
    "S256",
    "plain"
  ],
  "scopes_supported": [
    "openid",
    "email",
    "groups",
    "profile",
    "offline_access"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "claims_supported": [
    "iss",
    "sub",
    "aud",
    "iat",
    "exp",
    "email",
    "email_verified",
    "locale",
    "name",
    "preferred_username",
    "at_hash"
  ]
}
```

ولاحظ أن `grant_types_supported` لا تحتوي `client_credentials`، أي أن Dex بهذا الإصدار لا يدعم تدفق Client Credentials، وهذا مثال عملي على فائدة ملف الاكتشاف، فقد عرفنا ذلك قبل أن نرسل أي طلب.

### الخطوة 1: نولد قيم PKCE وstate وnonce

نقوم الآن بما يفعله التطبيق عند الضغط على زر الدخول، فنولد `code_verifier` عشوائياً بترميز Base64url، ثم نحسب منه `code_challenge` بالهاش SHA-256، ونولد `state` و`nonce`:

```bash
CODE_VERIFIER=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
STATE=$(openssl rand -hex 16)
NONCE=$(openssl rand -hex 16)
echo "verifier:  $CODE_VERIFIER"
echo "challenge: $CODE_CHALLENGE"
echo "state:     $STATE"
echo "nonce:     $NONCE"
```

والمخرج سوف يكون كما يلي، وستختلف القيم عندك في كل مرة:

```text
verifier:  hlevgkxvzBX8MBsyhDO5uPqn_njc8BGblgndF1iF88Abi1KuYibeF_nZhfxlD_Q5
challenge: bHo9Yc1ShfHSoIXnqoFZ8FKFO-lF26FESrnkRS8SKCs
state:     017136e38297e84b0efa637753f5ae0e
nonce:     83750df7dfd68a031597b6a8ae4445f8
```

ولاحظ أن الـ verifier طوله 64 حرفاً، والمواصفة تشترط بين 43 و128 حرفاً، وأن الـ challenge هو الشيء الوحيد من الاثنين الذي سوف يخرج في الرابط.

### الخطوتان 2 و3: رابط التفويض وتسجيل الدخول

نبني رابط التفويض بنفس المعاملات التي شرحناها في الخطوة 2 من التدفق:

```bash
echo "http://127.0.0.1:18556/dex/auth?response_type=code&client_id=notes-app&redirect_uri=http%3A%2F%2F127.0.0.1%3A18080%2Fcallback&scope=openid%20email%20profile%20offline_access&state=$STATE&nonce=$NONCE&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256"
```

افتح الرابط الناتج في المتصفح، فتظهر صفحة الدخول الخاصة بمزود الهوية، ولاحظ أنها صفحة Dex وليست صفحة التطبيق، وهنا فقط تكتب كلمة المرور:

![صفحة تسجيل الدخول في Dex وفيها حقل البريد alice@example.com وحقل كلمة المرور وزر Login](https://arabroot.io/content/images/2026/10/oauth-oidc-10-dex-login.webp)

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

### الخطوة 4: العودة ومعها الكود

بعد الدخول يحولك Dex إلى عنوان العودة، ولأنه لا يوجد تطبيق يعمل على المنفذ 18080 فسوف يعرض المتصفح صفحة خطأ في الاتصال، وهذا متوقع، فالمهم هو الرابط في شريط العنوان، وكان عندنا كما يلي:

```text
http://127.0.0.1:18080/callback?code=olzxfzy2dlxlwuvnn3awdcuck&state=017136e38297e84b0efa637753f5ae0e
```

ولاحظ أن قيمة `state` في الرابط هي نفسها التي ولدناها، وهذا ما يفحصه التطبيق قبل أي شيء آخر. وانسخ قيمة الكود إلى متغير، ولاحظ أن أمامك دقائق قليلة قبل أن تنتهي صلاحيته:

```bash
CODE=olzxfzy2dlxlwuvnn3awdcuck
```

### الخطوتان 5 و6: استبدال الكود بالتوكنات

قبل أن نرسل الطلب الصحيح، لنفترض أن مخترقاً رأى هذا الرابط في سجلات الـ Reverse Proxy، وهو يعرف أيضاً سر التطبيق، ولكنه لا يملك الـ `code_verifier`، فيرسل الكود مع قيمة من عنده:

```bash
curl -s -u notes-app:lab-only-secret-change-me \
  -d grant_type=authorization_code \
  -d code=$CODE \
  -d redirect_uri=http://127.0.0.1:18080/callback \
  -d code_verifier=wrong-verifier-from-an-attacker-0000000000000000 \
  http://127.0.0.1:18556/dex/token
```

```json
{"error":"invalid_grant","error_description":"Invalid code_verifier."}
```

رفض Dex الطلب رغم أن الكود والسر صحيحان، وهذه هي الحماية التي يضيفها PKCE. والآن يرسل التطبيق الحقيقي الطلب بالـ verifier الأصلي، ونحفظ الرد في ملف لنستخدمه بعد ذلك:

```bash
curl -s -u notes-app:lab-only-secret-change-me \
  -d grant_type=authorization_code \
  -d code=$CODE \
  -d redirect_uri=http://127.0.0.1:18080/callback \
  -d code_verifier=$CODE_VERIFIER \
  http://127.0.0.1:18556/dex/token | tee token.json
```

والمخرج سوف يكون كما يلي، وقد اختصرنا التوكنات الطويلة بعلامة «…»:

```json
{"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImE1MTU0YmRi…","token_type":"bearer","expires_in":3599,"refresh_token":"Chl2ZTJsdnJwYWhudWhuZWw0M21zMmZzdHV2Ehll…","id_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImE1MTU0YmRi…"}
```

في المخرج أعلاه لاحظ التالي:

- `token_type` قيمته `bearer`، أي أن من يحمل الـ Access Token يستطيع استخدامه.
- `expires_in` هي 3599 ثانية، أي ساعة كما ضبطناها في `expiry`.
- وصل `refresh_token` لأننا طلبنا النطاق `offline_access`، ووصل `id_token` لأننا طلبنا `openid`.

والآن نرسل نفس الطلب مرة ثانية بنفس الكود، كما لو أن المخترق استطاع أن ينسخ الطلب كاملاً بعد أن أرسله التطبيق:

```json
{"error":"invalid_grant","error_description":"Invalid or expired code parameter."}
```

فالكود يستخدم مرة واحدة فقط، وبعد ذلك يرفضه المزود. ويبقى الخطأ الثالث الذي سوف تقابله كثيراً وهو عنوان العودة، فإذا غيرت `redirect_uri` في رابط التفويض إلى أي عنوان آخر، ولو بإضافة شرطة مائلة في آخره (`/callback/`)، فلن يحولك Dex إلى أي مكان، وإنما يعرض صفحة خطأ فيها:

```text
Bad Request
Unregistered redirect_uri.
```

ولاحظ أن المزود لا يعيدك إلى العنوان الخاطئ ليخبر التطبيق بالخطأ، والسبب أنه لا يستطيع أن يثق بعنوان غير مسجل، فلو أرسل إليه أي شيء لكان قد أرسله إلى المخترق.

### نقرأ الـ ID Token

نستخرج الـ ID Token من الملف، ونفك الجزأين الأولين بدالة صغيرة تعيد علامات الحشو Padding التي يحذفها Base64url ثم تفك الترميز:

```bash
ID_TOKEN=$(sed -E 's/.*"id_token":"([^"]+)".*/\1/' token.json)
jwt_part() { echo "$1" | cut -d. -f"$2" | tr '_-' '/+' | awk '{while (length($0)%4) $0=$0"="; print}' | base64 -d; echo; }
jwt_part "$ID_TOKEN" 1
jwt_part "$ID_TOKEN" 2
```

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

```json
{"alg":"RS256","kid":"a5154bdbe8a5f9b797c90ecf1609931e4a4dfb5b"}
{"iss":"http://127.0.0.1:18556/dex","sub":"CiQwOGE4Njg0Yi1kYjg4LTRiNzMtOTBhOS0zY2QxNjYxZjU0NjYSBWxvY2Fs","aud":"notes-app","exp":1791234129,"iat":1791230529,"nonce":"83750df7dfd68a031597b6a8ae4445f8","at_hash":"dGSWogvUBu2ctFiJfP-_YA","c_hash":"s3s0vRcXvKGu3DAq3d04cA","email":"alice@example.com","email_verified":true,"name":"Alice"}
```

في المخرج أعلاه لاحظ التالي:

- لم نحتج أي مفتاح لقراءة المحتوى، وهذا ما قصدناه بأن Base64url ترميز وليس تشفيراً.
- `nonce` هي نفس القيمة التي ولدناها في الخطوة 1، و`aud` هو `notes-app`.
- `iat` و`exp` بالثواني منذ 1 يناير 1970 Unix Time، والأمر `date -u -d @1791230529` يحولها إلى `Mon Oct 5 20:02:09 UTC 2026`، والفرق بينهما ساعة واحدة.
- `sub` قيمة لا معنى لها للإنسان، لأن Dex يبنيها من معرف المستخدم واسم مصدر الدخول، والتطبيق يحفظها كما هي ولا يحاول تفسيرها.
- `at_hash` و`c_hash` هاش جزء من الـ Access Token والكود، يربطان الـ ID Token بهما حتى لا يبدل أحد واحداً منهما بآخر.

### نتحقق من التوقيع كما يفعل التطبيق

القراءة ليست تحققاً، فالآن نكتب ما تفعله أي مكتبة OIDC عند استلام الـ ID Token: تجلب ملف الاكتشاف ثم JWKS، وتختار المفتاح بالـ `kid`، وتتحقق من التوقيع، ثم تفحص الحقول. احفظ الملف التالي باسم `verify-id-token.mjs`، ولا يحتاج أي مكتبة خارجية لأن Node.js يقرأ مفاتيح JWK مباشرة:

```javascript
// Verifies an ID token the way a client must: signature with the IdP's public key (JWKS), then iss, aud, exp, nonce.
import { createPublicKey, verify } from 'node:crypto';

const [token, expectedNonce] = process.argv.slice(2);
const ISSUER = 'http://127.0.0.1:18556/dex';
const CLIENT_ID = 'notes-app';

const [h, p, s] = token.split('.');
const header = JSON.parse(Buffer.from(h, 'base64url'));
const payload = JSON.parse(Buffer.from(p, 'base64url'));

// 1) the IdP publishes its public keys in jwks_uri (from the discovery document)
const { jwks_uri } = await (await fetch(`${ISSUER}/.well-known/openid-configuration`)).json();
const { keys } = await (await fetch(jwks_uri)).json();
const jwk = keys.find(k => k.kid === header.kid);
if (!jwk || header.alg !== 'RS256') throw new Error('unknown key or unexpected alg');

// 2) the signature covers "header.payload" exactly as it was sent
const ok = verify('RSA-SHA256', Buffer.from(`${h}.${p}`), createPublicKey({ key: jwk, format: 'jwk' }), Buffer.from(s, 'base64url'));
console.log('signature :', ok ? 'valid' : 'INVALID');

// 3) the claims
const now = Math.floor(Date.now() / 1000);
console.log('iss       :', payload.iss === ISSUER ? 'ok' : 'WRONG');
console.log('aud       :', payload.aud === CLIENT_ID ? 'ok' : 'WRONG');
console.log('exp       :', payload.exp > now ? `ok (${payload.exp - now}s left)` : 'EXPIRED');
console.log('nonce     :', payload.nonce === expectedNonce ? 'ok' : 'WRONG');
```

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

- الخوارزمية يحددها التطبيق مسبقاً (`RS256`)، ولا يأخذها من ترويسة التوكن ثقة بها، والسبب أن ترويسة التوكن يكتبها من صنع التوكن، وقد ظهرت ثغرات في مكتبات JWT كانت تقبل `"alg":"none"` أي توكن دون توقيع، لذلك ينص [RFC 8725 لأفضل ممارسات JWT](https://datatracker.ietf.org/doc/html/rfc8725?ref=arabroot.io) على أن يحدد التطبيق الخوارزميات المسموحة بنفسه.
- التوقيع يحسب على النص `header.payload` كما وصل، وليس على JSON بعد إعادة ترتيبه.
- في التطبيق الحقيقي تحفظ JWKS مدة قصيرة بدلاً من جلبها مع كل طلب، وتسمح بفارق دقيقة أو اثنتين في `exp` لاختلاف الساعات.

ونشغله بالتوكن والـ nonce:

```bash
node verify-id-token.mjs "$ID_TOKEN" "$NONCE"
```

```text
signature : valid
iss       : ok
aud       : ok
exp       : ok (3572s left)
nonce     : ok
```

والآن نكسره عمداً: نغير البريد في المحتوى إلى `admin@example.com` ونبقي الترويسة والتوقيع كما هما، وهذا بالضبط ما قد يحاوله شخص يريد أن يدخل باسم المدير:

```bash
TAMPERED=$(node -e '
const [h, p, s] = process.argv[1].split(".");
const claims = JSON.parse(Buffer.from(p, "base64url"));
claims.email = "admin@example.com";
console.log([h, Buffer.from(JSON.stringify(claims)).toString("base64url"), s].join("."));
' "$ID_TOKEN")
node verify-id-token.mjs "$TAMPERED" "$NONCE"
```

```text
signature : INVALID
iss       : ok
aud       : ok
exp       : ok (3572s left)
nonce     : ok
```

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

### نستدعي UserInfo بالـ Access Token

الآن نقوم بالخطوة 7، فنستخدم الـ Access Token في طلب إلى نقطة UserInfo، ثم نرسل نفس الطلب دون توكن:

```bash
ACCESS_TOKEN=$(sed -E 's/.*"access_token":"([^"]+)".*/\1/' token.json)
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:18556/dex/userinfo
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18556/dex/userinfo
```

```text
{"iss":"http://127.0.0.1:18556/dex","sub":"CiQwOGE4Njg0Yi1kYjg4LTRiNzMtOTBhOS0zY2QxNjYxZjU0NjYSBWxvY2Fs","aud":"notes-app","exp":1791234129,"iat":1791230529,"nonce":"83750df7dfd68a031597b6a8ae4445f8","at_hash":"ogLtOTPU-XI6r54a-zwwmw","email":"alice@example.com","email_verified":true,"name":"Alice"}
401
```

ولاحظ أن نقطة UserInfo ردت بنفس `sub` الموجود في الـ ID Token، وهذا شرط في المواصفة يجب أن يفحصه التطبيق، وأن الطلب دون توكن رفض بالرمز 401.

### نجدد التوكن ونرى التدوير Rotation

نرسل الـ Refresh Token إلى نقطة التوكن لنحصل على توكنات جديدة:

```bash
REFRESH_TOKEN=$(sed -E 's/.*"refresh_token":"([^"]+)".*/\1/' token.json)
curl -s -u notes-app:lab-only-secret-change-me \
  -d grant_type=refresh_token \
  -d refresh_token=$REFRESH_TOKEN \
  http://127.0.0.1:18556/dex/token | tee token2.json
```

```json
{"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImE1MTU0YmRi…","token_type":"bearer","expires_in":3599,"refresh_token":"Chl2ZTJsdnJwYWhudWhuZWw0M21zMmZzdHV2Ehl4…","id_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImE1MTU0YmRi…"}
```

ولاحظ أن الـ Refresh Token في الرد يختلف عن القديم، لأن Dex يدور Rotate توكن التجديد مع كل استخدام. والآن ننتظر بضع ثوان ونستخدم الـ Refresh Token القديم مرة أخرى، كما لو أن أحداً سرقه قبل أن يستخدمه التطبيق:

```bash
curl -s -u notes-app:lab-only-secret-change-me \
  -d grant_type=refresh_token \
  -d refresh_token=$REFRESH_TOKEN \
  http://127.0.0.1:18556/dex/token
```

```json
{"error":"invalid_request","error_description":"Refresh token is invalid or has already been claimed by another client."}
```

فالتوكن القديم لم يعد صالحاً، ويسمح Dex باستخدامه مرة ثانية خلال 3 ثوان فقط بالإعداد الافتراضي `reuseInterval`، حتى لا يفشل طلبان متزامنان من نفس التطبيق.

### نجرب تدفق الأجهزة

نقوم الآن بدور أداة سطر أوامر تريد تسجيل الدخول، فنطلب كوداً بالتطبيق العام `cli-tool` الذي لا يملك سراً:

```bash
curl -s -d client_id=cli-tool -d "scope=openid email" http://127.0.0.1:18556/dex/device/code | tee device.json
```

```json
{
   "device_code": "pnf3qooo6rra3sayq64a4zgu7ex7rbd5zuksnjgllzttcfsyk3u",
   "user_code": "MQSM-SHKK",
   "verification_uri": "http://127.0.0.1:18556/dex/device",
   "verification_uri_complete": "http://127.0.0.1:18556/dex/device?user_code=MQSM-SHKK",
   "expires_in": 300,
   "interval": 5
}
```

في المخرج أعلاه لاحظ التالي: `user_code` قصير وسهل الكتابة وهو ما تعرضه الأداة للمستخدم، و`device_code` طويل ويبقى عند الأداة لتسأل به عن النتيجة، و`interval` يقول للأداة ألا تسأل أكثر من مرة كل 5 ثوان، و`expires_in` يعطي المستخدم 5 دقائق ليوافق. والآن تسأل الأداة نقطة التوكن قبل أن يوافق المستخدم:

```bash
DEVICE_CODE=$(sed -nE 's/.*"device_code": "([^"]+)".*/\1/p' device.json)
curl -s -d grant_type=urn:ietf:params:oauth:grant-type:device_code \
  -d device_code=$DEVICE_CODE \
  -d client_id=cli-tool \
  http://127.0.0.1:18556/dex/token
```

```json
{"error":"authorization_pending"}
```

افتح `verification_uri` في المتصفح، واكتب الكود الذي عرضته الأداة، ثم سجل الدخول بنفس الحساب:

![صفحة Enter User Code في Dex وفيها حقل الكود وزر Submit](https://arabroot.io/content/images/2026/10/oauth-oidc-09-device-code.webp)

يكتب المستخدم الكود من جهاز فيه متصفح

وبعد الدخول تظهر رسالة تطلب منك العودة إلى جهازك:

![صفحة Dex تعرض Login Successful for CLI Tool وتحتها Return to your device to continue](https://arabroot.io/content/images/2026/10/oauth-oidc-11-device-done.webp)

انتهى دور المتصفح، والتوكنات تذهب إلى الأداة وليس إلى هذه الصفحة

ونعيد نفس طلب الأداة، فيرد هذه المرة بالتوكنات (اختصرناها):

```json
{"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImE1MTU0YmRi…","token_type":"bearer","expires_in":3599,"id_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImE1MTU0YmRi…"}
```

ولاحظ أن الأداة لم تر كلمة المرور، ولم تحتج متصفحاً، ولم يصلها Refresh Token لأنها لم تطلب `offline_access`.

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

وبعد الانتهاء نحذف الـ Container والشبكة والـ Image، ثم الملفات التي تحمل التوكنات:

```bash
docker compose down -v
docker rmi ghcr.io/dexidp/dex:v2.45.1
rm -f token.json token2.json device.json
```

## التوكنات والجلسات وتسجيل الخروج

بعد أن يحصل التطبيق على التوكنات يبقى سؤال عملي: أين يحفظها، وكم تعيش، وماذا يحدث عندما يضغط المستخدم «تسجيل الخروج»؟

### أين تحفظ التوكنات؟

الحل الأسهل في تطبيقات الصفحة الواحدة SPA أن تحفظ التوكنات في `localStorage` داخل المتصفح، وهذه الطريقة غير مناسبة إطلاقاً، والسبب أن أي سكربت يعمل في الصفحة يستطيع قراءة `localStorage`، فإذا وجد المخترق ثغرة حقن سكربت XSS واحدة في أي صفحة، أو اخترق مكتبة JavaScript تحملها الصفحة، فإنه يأخذ التوكنات ويستخدمها من جهازه، والـ Refresh Token يبقى صالحاً أياماً بعد ذلك. والحل الأنسب هو نمط الخلفية للواجهة Backend for Frontend واختصاراً BFF، الذي توصي به [مسودة OAuth للتطبيقات التي تعمل في المتصفح](https://datatracker.ietf.org/doc/draft-ietf-oauth-browser-based-apps/?ref=arabroot.io)، حيث يقوم سيرفر التطبيق بدور الـ Client، فيحفظ التوكنات عنده بجانب الجلسة، ولا يعطي المتصفح إلا ملف تعريف ارتباط Cookie للجلسة، لا يستطيع JavaScript أن يقرأه:

![مخطط يقارن تطبيقاً يحفظ الـ Access Token والـ Refresh Token في localStorage حيث يقرؤهما سكربت محقون ويرسلهما إلى سيرفر المخترق، بتطبيق يحفظ التوكنات في السيرفر ولا يعطي المتصفح إلا Cookie من نوع HttpOnly](https://arabroot.io/content/images/2026/10/oauth-oidc-08-token-storage.webp)

في المتصفح يصل إلى التوكن أي سكربت، وفي السيرفر لا يراه المتصفح أصلاً

وتطبيقاتك المستضافة ذاتياً مثل Gitea وOutline وGrafana تعمل بهذا الشكل أصلاً، فهي تطبيقات تعمل على السيرفر وتحفظ التوكنات عندها، والمهم أن يكون ملف تعريف الارتباط للجلسة بالخصائص `Secure` و`HttpOnly` و`SameSite`، وقد شرحنا كل خاصية منها وكيف تضيفها عبر الـ Reverse Proxy في قسم [ملفات تعريف الارتباط](https://arabroot.io/articles/%D8%AA%D8%B1%D9%88%D9%8A%D8%B3%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%85%D8%A7%D9%86-hsts-%D9%88-csp/#cookies) من دليل ترويسات الأمان.

### مدة صلاحية التوكنات وتدوير Refresh Token

القاعدة أن يكون عمر التوكن بحجم الضرر إذا سرق، لذلك يكون الـ Access Token قصيراً، من 5 دقائق إلى ساعة، لأنه يرسل مع كل طلب إلى الـ API ويمر في أماكن أكثر، والـ Refresh Token أطول، من ساعات إلى أسابيع، لأنه لا يذهب إلا إلى مزود الهوية. وتدوير Refresh Token Rotation الذي رأيناه في التجربة يعني أن كل استخدام يعطي توكن تجديد جديداً ويلغي القديم، فإذا سرق التوكن واستخدمه المخترق ثم حاول التطبيق الحقيقي استخدامه، أو العكس، يكتشف المزود أن التوكن استخدم مرتين، ويستطيع أن يلغي السلسلة كلها. ولهذا ينص [RFC 9700](https://datatracker.ietf.org/doc/html/rfc9700?ref=arabroot.io#section-2.2.2) على أن توكنات التجديد للتطبيقات العامة يجب أن تكون مرتبطة بالجهاز الذي صدرت له Sender-Constrained، أو أن تدور مع كل استخدام.

📌

في 12 أبريل 2022 بدأ فريق الأمان في GitHub تحقيقاً كشف أن مخترقاً استخدم توكنات OAuth مسروقة كانت صادرة لتطبيقين من خارج GitHub هما Heroku وTravis CI، فسرد بها المؤسسات والمستودعات الخاصة لعدد كبير من المستخدمين الذين ربطوا حساباتهم بهذين التطبيقين، ونزل محتوى بعضها، ومنها مستودعات خاصة بـ npm، كما نشرت GitHub في [تنبيهها الأمني](https://github.blog/news-insights/company-news/security-alert-stolen-oauth-user-tokens/?ref=arabroot.io). ولم تكن التوكنات قد سرقت من GitHub، وإنما من التطبيقين اللذين كانا يحفظانها، وكانت تحمل صلاحيات واسعة على المستودعات الخاصة. والدرس أن التوكن في يد تطبيق آخر جزء من سطح الهجوم Attack Surface لحسابك، لذلك اطلب أقل النطاقات، واجعل عمر التوكن قصيراً، وراجع التطبيقات المربوطة بحساباتك واسحب ما لم تعد تستخدمه.

### تسجيل الخروج والإلغاء Revocation

عندما تتذكر أن هناك ثلاث جلسات منفصلة يصبح تسجيل الخروج واضحاً: جلسة التطبيق، وجلسة مزود الهوية، والتوكنات التي في يد التطبيق، وزر الخروج في التطبيق يحذف الجلسة الأولى فقط، فإذا ضغطت «تسجيل الدخول» بعدها مباشرة يعيدك مزود الهوية دون كلمة مرور لأن جلسته ما زالت قائمة. والأدوات المتاحة لإغلاق الباقي هي:

- **إلغاء التوكن Token Revocation:** يرسل التطبيق التوكن إلى نقطة الإلغاء في مزود الهوية كما في [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009?ref=arabroot.io)، فلا يعود صالحاً، وهذا مهم خاصة للـ Refresh Token.
- **الخروج الذي يبدؤه التطبيق RP-Initiated Logout:** يحول التطبيق المستخدم إلى نقطة الخروج في مزود الهوية بعد أن يحذف جلسته، فيحذف المزود جلسته أيضاً، ثم يعيده إلى عنوان مسجل، كما في [مواصفة RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1%5F0.html?ref=arabroot.io).
- **الخروج عبر القناة الخلفية Back-Channel Logout:** عندما يسجل المستخدم الخروج في مزود الهوية، أو يعطله المدير، يرسل المزود طلباً مباشراً إلى كل تطبيق مسجل ليحذف جلسة هذا المستخدم، كما في [مواصفة Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1%5F0.html?ref=arabroot.io)، ودعم هذه المواصفة يختلف من تطبيق لآخر، لذلك تأكد منه في توثيق كل تطبيق قبل أن تعتمد عليه.

## قائمة أفضل الممارسات

هذه القائمة تجمع ما سبق في نقاط تراجعها عند ربط أي تطبيق بمزود الهوية، ومعظمها مأخوذ من [RFC 9700](https://datatracker.ietf.org/doc/html/rfc9700?ref=arabroot.io):

- **استخدم Authorization Code مع PKCE بالطريقة S256** لكل تطبيق فيه مستخدم، والسبب أنه يحمي من سرقة الكود وحقنه حتى مع التطبيقات السرية.
- **سجل عناوين العودة كاملة ومحددة** دون نجمة ودون مسارات عامة، والسبب أن أي مرونة في المطابقة تفتح باباً لتحويل الكود إلى المخترق.
- **اطلب أقل النطاقات** التي يحتاجها التطبيق فعلاً، فتطبيق يحتاج الاسم والبريد لا يحتاج `groups` أو `offline_access`، والسبب أن ضرر التوكن المسروق بحجم نطاقاته.
- **احفظ Client Secret في السيرفر فقط**، في ملف `.env` أو مدير أسرار Secrets Manager، ولا تقم بوضعه في كود يعمل في المتصفح أو تطبيق جوال أو مستودع Git، والسبب أن السر المكشوف يحول تطبيقك السري إلى تطبيق عام دون أن تعلم.
- **تحقق من الـ ID Token كاملاً:** التوقيع بمفتاح من JWKS، و`iss` و`aud` و`exp` و`nonce`، وحدد الخوارزميات المسموحة بنفسك، ولا تكتب هذا بيدك في تطبيق حقيقي وإنما استخدم مكتبة OIDC معروفة.
- **اربط الحساب بالحقلين `iss` و`sub` معاً** وليس بالبريد، والسبب أن البريد قد يتغير أو يعاد استخدامه، وأن مزودين مختلفين قد يعطيان نفس البريد لشخصين مختلفين.
- **لا تقم بوضع التوكنات في الروابط أو السجلات**، فلا ترسل الـ Access Token كمعامل في الرابط `?access_token=` وإنما في الترويسة `Authorization`، وتأكد أن سجلات التطبيق والـ Reverse Proxy لا تكتب ترويسة `Authorization` ولا جسم طلبات `/token`.
- **اجعل الأعمار قصيرة وفعل التدوير:** Access Token بالدقائق، وRefresh Token يدور مع كل استخدام، والسبب أن ذلك يحدد مدة استفادة المخترق من أي توكن يسرقه.
- **احفظ التوكنات في السيرفر** مع Cookie للجلسة بالخصائص `Secure` و`HttpOnly` و`SameSite`، ولا تحفظها في `localStorage`.
- **اضبط الساعة في كل سيرفر** بخدمة NTP، واسمح بفارق صغير لا يزيد على دقيقتين في فحص `exp` و`iat`، والسبب أن السيرفر الذي تتأخر ساعته يرفض توكنات صحيحة أو يقبل توكنات منتهية.
- **استخدم HTTPS في كل مكان** عدا التجربة المحلية على `127.0.0.1`، والسبب أن OAuth 2.0 يعتمد على TLS لحماية التوكنات من نوع Bearer أثناء نقلها.

## أخطاء شائعة وكيف تعرفها

- **الخطأ `redirect_uri_mismatch` أو «Unregistered redirect\_uri»:** انسخ قيمة `redirect_uri` من رابط التفويض في شريط العنوان، وقارنها بالمسجلة حرفاً بحرف، فإذا كانت تبدأ بـ `http://` أو فيها منفذ داخلي فالتطبيق خلف Reverse Proxy ولا يعرف عنوانه العام، وحله في قسم [redirect\_uri\_mismatch](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/#oauth-mismatch) من دليل التطبيق خلف Reverse Proxy.
- **الخطأ `invalid_grant` عند استبدال الكود:** الكود استخدم من قبل (غالباً لأن المستخدم أعاد تحميل صفحة العودة)، أو انتهت صلاحيته، أو الـ `code_verifier` لا يطابق لأن التطبيق فقد جلسته بين الخطوة 1 والخطوة 5، وهذا يحدث عندما يعمل التطبيق على أكثر من نسخة Instance دون جلسات مشتركة.
- **رفض `state` أو حلقة تحويل لا تنتهي:** ملف تعريف الارتباط الذي يحفظ `state` لم يرجع مع طلب العودة، وأشهر أسبابه `SameSite=Strict` على ملف الجلسة، أو اختلاف النطاق بين بداية الدخول ونهايته، مثل `example.com` و`www.example.com`.
- **الخطأ «issuer does not match»:** قيمة `iss` في التوكن لا تطابق ما كتبته في التطبيق، وكثيراً ما يكون الفرق شرطة مائلة في الآخر، أو أن التطبيق يصل إلى المزود بعنوان داخلي والمزود يعلن عنوانه العام.
- **التوكن منتهي فور إصداره:** ساعة أحد السيرفرات متأخرة أو متقدمة، فقارن `date -u` على سيرفر التطبيق وسيرفر المزود.
- **لا يصل Refresh Token:** التطبيق لم يطلب `offline_access`، أو المزود لا يسمح بهذا النطاق للتطبيق، كما في authentik منذ الإصدار 2024.2.
- **إرسال الـ ID Token إلى الـ API:** يعمل في البداية لأن الـ API يقرأ JWT صحيحاً، ولكنه يقبل توكناً لم يصدر من أجله، فاجعل الـ API يفحص `aud` ويرفض أي توكن ليس موجهاً إليه.

## OAuth وOIDC وSAML وLDAP: ما الفرق؟

سوف تقابل هذه الأسماء معاً في كل مزود هوية، وهي لا تتنافس على نفس الدور، والجدول التالي يضع كل واحد في مكانه، وسوف نخصص لكل من SAML وLDAP مقالاً مستقلاً بإذن الله:

| البروتوكول                                                                  | يجيب عن سؤال                                           | الشكل                                         | أين تراه                                                                                                                                                                                                        |
| --------------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OAuth 2.0                                                                   | ما الذي يسمح لهذا التطبيق أن يفعله؟ (تفويض)            | توكنات عبر HTTP وJSON                         | APIs، وتطبيقات تتصل بحساباتك في خدمات أخرى                                                                                                                                                                      |
| OpenID Connect                                                              | من هو المستخدم الذي دخل الآن؟ (هوية فوق OAuth)         | ID Token بصيغة JWT                            | أزرار «الدخول بحساب…»، وربط Gitea وOutline وGrafana بمزود الهوية                                                                                                                                                |
| [SAML 2.0](https://docs.oasis-open.org/security/saml/v2.0/?ref=arabroot.io) | من هو المستخدم؟ (هوية، الجيل الأقدم)                   | رسائل XML موقعة عبر المتصفح                   | أنظمة المؤسسات الكبيرة والخدمات السحابية التجارية                                                                                                                                                               |
| [LDAP](https://datatracker.ietf.org/doc/html/rfc4511?ref=arabroot.io)       | أين دليل المستخدمين والمجموعات، وهل كلمة المرور صحيحة؟ | بروتوكول دليل Directory على المنفذ 389 أو 636 | Active Directory، والتطبيقات القديمة التي تتحقق من كلمة المرور بنفسها                                                                                                                                           |
| SCIM                                                                        | كيف تنشأ الحسابات وتحذف في التطبيقات تلقائياً؟         | REST API بصيغة JSON                           | شرحناه في [ما هو 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/) |

والقاعدة العملية لأي تطبيق جديد تربطه بمزود الهوية: إذا دعم OpenID Connect فاختره، وإذا كان يدعم SAML فقط فهو الخيار التالي، أما LDAP فاتركه للتطبيقات التي لا تدعم غيره، لأن التطبيق في LDAP يستلم كلمة مرور المستخدم بنفسه، أي أنه يعيدنا إلى المشكلة التي بدأنا بها هذا المقال.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- جاء OAuth لينهي مشاركة كلمة المرور مع التطبيقات، فيأخذ كل تطبيق توكن محدوداً بنطاقات ومدة، ويمكن سحبه وحده، ولا يرى كلمة المرور أبداً.
- OAuth 2.0 للتفويض، وOpenID Connect يضيف فوقه إثبات الهوية بالـ ID Token، فالـ ID Token للتطبيق ليعرف من دخل، والـ Access Token للـ API ليعرف ما المسموح.
- تدفق Authorization Code مع PKCE هو التدفق الوحيد الذي تحتاجه لأي تطبيق فيه مستخدم، والكود يمر في المتصفح والتوكنات لا تمر إلا بين السيرفرات.
- Client Credentials لتطبيق يعمل باسمه، وDevice Flow لجهاز دون متصفح، أما Implicit وPassword فلا يستخدمان في أي تطبيق جديد.
- JWT يقرؤه أي أحد، والتوقيع بالمفتاح الخاص لمزود الهوية هو الذي يكشف أي تعديل، فتحقق منه بالمفتاح العام من JWKS ثم افحص `iss` و`aud` و`exp` و`nonce`.
- عناوين العودة تطابق حرفاً بحرف، والأسرار تبقى في السيرفر، والتوكنات أعمارها قصيرة وتدور، وتسجيل الخروج يشمل التطبيق ومزود الهوية والتوكنات معاً.

والآن عندما تعود إلى أدلتنا العملية سوف تعرف سبب كل حقل تكتبه، فابدأ بها:

[![](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/authentik-forward-auth-cover-cover.webp)دليلكيف تحمي أي تطبيق ويب بصفحة دخول authentik عبر Forward Authكيف تضع صفحة دخول authentik مع المصادقة الثنائية أمام أي أداة داخلية دون أن تعدل في التطبيق نفسه.](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D8%A3%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-authentik-forward-auth/)[![](https://arabroot.io/content/images/2026/10/authentik-google-cover-cover.webp)دليلتسجيل الدخول بحساب Google في authentik وقصره على نطاق مؤسستككيف تضيف زر الدخول بحساب Google إلى authentik، وتقصر التسجيل على حسابات نطاق مؤسستك وحده.](https://arabroot.io/articles/%D8%AA%D8%B3%D8%AC%D9%8A%D9%84-%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%A8%D8%AD%D8%B3%D8%A7%D8%A8-google-%D9%81%D9%8A-authentik/)[![](https://arabroot.io/content/images/2026/10/gitea-authentik-cover-cover.webp)دليلدخول موحد إلى Gitea بحسابات authentikاجعل موظفيك يدخلون إلى Gitea بحساباتهم في authentik، فتنشأ حساباتهم تلقائياً وتتبع صلاحياتهم المجموعة التي ينتمون إليها.](https://arabroot.io/articles/%D8%B1%D8%A8%D8%B7-gitea-%D9%85%D8%B9-authentik-%D8%B9%D8%A8%D8%B1-oidc/)[![](https://arabroot.io/content/images/2026/10/security-headers-cover-cover.webp)دليلHSTS وCSP: ترويسات الأمان التي يحتاجها كل تطبيقما الذي تطلبه كل ترويسة أمان من المتصفح، ولماذا يصعب التراجع عن HSTS، وكيف تبني سياسة CSP لا تكسر تطبيقك.](https://arabroot.io/articles/%D8%AA%D8%B1%D9%88%D9%8A%D8%B3%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%85%D8%A7%D9%86-hsts-%D9%88-csp/)

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

- أكتوبر 2026: كتابة المقال، وتنفيذ تدفق Authorization Code مع PKCE وتدفق الأجهزة وتجديد التوكن على Dex v2.45.1 مع curl وNode.js.