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

# تثبيت Keycloak مزود الهوية IdP الأشهر في المؤسسات
- URL: https://arabroot.io/articles/تثبيت-keycloak-مزود-الهوية-للمؤسسات/
- Published: 2026-10-25T19:34:27.000Z
- Updated: 2026-10-26T08:02:08.000Z
- Description: نثبت Keycloak مع PostgreSQL، ونربط به تطبيقاً عبر OIDC، ونفرض المصادقة الثنائية على الجميع، ونقيس ما يحتاجه من ذاكرة.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, الاستضافة الذاتية, Keycloak

في [دليل 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/) بنينا مزود هوية Identity Provider خفيفاً يناسب فريقاً صغيراً، ولكن عندما تدخل مؤسسة أكبر قليلاً، أو تعمل مع جهة حكومية أو بنك، فسوف تجد أن الاسم الذي يتكرر في كراسات الشروط وفي وثائق الأنظمة الجاهزة هو [Keycloak](https://www.keycloak.org/?ref=arabroot.io)، حيث تأتي كثير من الأنظمة المؤسسية بدليل جاهز لربطها به عبر OIDC أو SAML، ويعرفه مهندسو الأنظمة الذين سوف توظفهم، وتبني عليه شركات كبيرة منتجاتها التجارية. لذلك فالسؤال عادة ليس هل تستخدم مزود هوية، وإنما أي مزود يناسب حجمك، وKeycloak هو الإجابة عندما تحتاج إلى نطاقات Realms منفصلة، وربط مع Active Directory أو LDAP، وتدفقات دخول Authentication Flows تبنيها خطوة خطوة.

ولكن Keycloak تطبيق Java كبير، فإذا ثبته كما في المثال الأول من التوثيق فسوف يعمل، ولكنه سوف يقلع في دقائق بدل ثوان، ويأخذ من الذاكرة RAM ما يتسع له السيرفر، ويترك الحماية من التخمين Brute Force معطلة، ولا يفرض المصادقة الثنائية 2FA على أحد. وهذه الأمور كلها لا تظهر لك في أول تجربة، وإنما تظهر عندما يكون النظام في بيئة الإنتاج Production.

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

- ما هو Keycloak، ورخصته، وسرعة صدور إصداراته الأمنية، ومفاهيمه الأساسية: النطاق Realm والتطبيق Client والتدفق Flow.
- تثبيته مع PostgreSQL باستخدام Docker Compose، مع Docker Image مبنية مسبقاً Optimized تقلع في نصف دقيقة.
- إنشاء نطاق ومستخدمين ومجموعات، وربط تطبيق تجريبي عبر OIDC مع PKCE، وقراءة ما يصل إليه في الـ ID Token.
- واجهة الدخول بالعربية واتجاه RTL، وما ينقص الترجمة.
- فرض رمز TOTP على كل المستخدمين، والحماية من التخمين، والخطأ الذي يجعل صفحة الدخول ترفض الجميع.
- الذاكرة والقرص بأرقام مقيسة، والـ Reverse Proxy مع TLS، والنسخ الاحتياطي والاستعادة، والتحديث.

وإذا كانت المفاهيم الأساسية جديدة عليك فابدأ بهذه المقالات، حيث نفترض هنا أنك تعرف ما هو OIDC وما هو الـ ID Token:

[![](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/passkeys-cover-cover.webp)دليلالدخول دون كلمة مرور: شرح Passkeys وWebAuthnلماذا لا تحميك رموز SMS وTOTP من التصيد الحديث، وكيف تحل Passkeys المشكلة بمفتاح خاص لا يغادر جهازك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AF%D8%AE%D9%88%D9%84-%D8%AF%D9%88%D9%86-%D9%83%D9%84%D9%85%D8%A9-%D9%85%D8%B1%D9%88%D8%B1-passkeys-%D9%88-fido2/)

## ما هو Keycloak؟

Keycloak هو مزود هوية مفتوح المصدر Open Source بدأته Red Hat، ويصدر [برخصة Apache 2.0](https://github.com/keycloak/keycloak/blob/main/LICENSE.txt?ref=arabroot.io)، وهي رخصة تسمح لك بتشغيله وتعديله داخل منتج تجاري دون أن تنشر كودك، وقد [قبلته مؤسسة CNCF](https://www.cncf.io/projects/keycloak/?ref=arabroot.io) في 10 أبريل 2023 بمستوى Incubating. ويتحدث Keycloak البروتوكولات الثلاثة التي تحتاجها المؤسسات: OpenID Connect وOAuth 2.0 للتطبيقات الحديثة، وSAML 2.0 للأنظمة القديمة، ويستطيع أن يقرأ المستخدمين من LDAP أو Active Directory بدل أن ينسخهم، وهذه الخاصية اسمها اتحاد المستخدمين User Federation.

والمشروع نشط جداً، فآخر إصدار عند كتابة هذا الدليل هو [26.8.1](https://github.com/keycloak/keycloak/releases/tag/26.8.1?ref=arabroot.io) في 13 أكتوبر 2026، وقبله 26.8.0 في 1 أكتوبر، و26.7.5 في 30 سبتمبر. وهذه السرعة لها جانب يجب أن تنتبه له، حيث نشر المشروع في [صفحة التنبيهات الأمنية](https://github.com/keycloak/keycloak/security/advisories?ref=arabroot.io) 17 تنبيهاً منذ بداية 2026، منها 12 بدرجة High، وضم الإصدار 26.8.1 وحده إصلاحات لنحو عشرين ثغرة، منها ثغرة CVE-2026-94218 التي تسمح بتجاوز إلزام المستخدم بإعداد المصادقة الثنائية، وهي بالضبط الخاصية التي سوف نعتمد عليها في هذا الدليل. لذلك فتحديث Keycloak ليس مهمة ربع سنوية، وإنما مهمة تتابعها كل أسبوعين أو ثلاثة.

### المفاهيم التي سوف تتعامل معها كل يوم

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

- **النطاق Realm:** مساحة مستقلة لها مستخدموها ومجموعاتها وتطبيقاتها وسياساتها، ولا يرى نطاق ما في نطاق آخر. ويأتي Keycloak بنطاق اسمه `master`، وهذا النطاق للمديرين فقط، ولا تضع فيه موظفيك ولا تطبيقاتك، وإنما [أنشئ نطاقاً لمؤسستك](https://www.keycloak.org/docs/latest/server%5Fadmin/index.html?ref=arabroot.io#the-master-realm).
- **التطبيق Client:** كل تطبيق يسجل الدخول عبر Keycloak، وله معرف Client ID وسر Client Secret إن كان تطبيقاً سرياً Confidential، وقائمة بعناوين العودة Redirect URIs المسموحة.
- **المستخدمون Users والمجموعات Groups والأدوار Roles:** الحسابات وطريقة تجميعها، ومنها تبني صلاحيات كل تطبيق.
- **التدفق Authentication Flow:** سلسلة الخطوات التي يمر بها المستخدم عند الدخول، مثل كلمة المرور ثم رمز TOTP، وكل خطوة لها متطلب Requirement: إلزامية Required، أو بديلة Alternative، أو مشروطة Conditional، أو معطلة Disabled.

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

![سيرفر Keycloak يضم نطاق master للمديرين ونطاقاً للمؤسسة فيه المستخدمون والمجموعات والتطبيقات وتدفق الدخول، ومسار الدخول من المتصفح إلى التطبيق بأربع خطوات](https://arabroot.io/content/images/2026/10/keycloak-01-realm-map.webp)

نطاق master للمديرين، ونطاق مستقل للمؤسسة، ومسار الدخول عبر OIDC

## المتطلبات

- سيرفر Linux بمعالجين 2 vCPU على الأقل، وذاكرة 2 GB تكفي لـ Keycloak وPostgreSQL معاً إذا حددت حداً للذاكرة كما سيأتي، و4 GB أفضل إذا كانت على السيرفر تطبيقات أخرى.
- مساحة قرص بنحو 3 GB، والسبب أن الـ Image وحدها 751 MB، والنسخة المبنية مسبقاً مثلها، أما قاعدة البيانات فصغيرة جداً، حيث كانت 13 MB بعد إعداد النطاق والمستخدمين في هذا الدليل.
- Docker Engine مع Compose v2، وإذا لم يكن مثبتاً فاتبع [دليل تثبيت Docker](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/) أولاً.
- نطاق فرعي Subdomain مثل `auth.example.com` يشير إلى السيرفر، وReverse Proxy يصدر شهادة TLS، مثل [Caddy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/) أو [Nginx Proxy Manager](https://arabroot.io/articles/nginx-proxy-manager-%D9%86%D8%B7%D8%A7%D9%82%D8%A7%D8%AA-%D9%88%D8%B4%D9%87%D8%A7%D8%AF%D8%A7%D8%AA-tls/).
- حساب SMTP لرسائل استعادة كلمة المرور والتحقق من البريد، وتستطيع تأجيله أثناء التجربة.

## التثبيت

### لماذا نبني Docker Image خاصة؟

Keycloak مبني على إطار Quarkus، وهذا الإطار يجهز التطبيق في خطوة اسمها البناء Build بحسب الإعدادات التي لا تتغير، مثل نوع قاعدة البيانات وتفعيل نقاط الفحص Health Endpoints. فإذا شغلت الـ Image الرسمية مباشرة بالأمر `start` فسوف يكتشف Keycloak أن هذه الإعدادات تغيرت وينفذ خطوة البناء في كل مرة تنشئ فيها Container جديداً، وعند تشغيله أول مرة استغرقت هذه الخطوة وحدها 134 ثانية، ثم احتاج Keycloak إلى 140 ثانية أخرى لإنشاء جداول قاعدة البيانات، فكان المجموع 5 دقائق و17 ثانية حتى أجاب أول طلب. لذلك [يوصي التوثيق](https://www.keycloak.org/server/containers?ref=arabroot.io) ببناء Image خاصة تنفذ فيها خطوة البناء مرة واحدة، ثم تشغلها بالخيار `--optimized`، وبهذه الطريقة صار الـ Container جاهزاً Healthy في 34 ثانية.

### هيكل المجلد والأسرار Secrets

```bash
sudo mkdir -p /opt/keycloak
cd /opt/keycloak
echo "PG_PASS=$(openssl rand -hex 24)" >> .env
echo "KC_ADMIN_PASS=$(openssl rand -hex 16)" >> .env
chmod 600 .env
```

ويحمل الملف `.env` سرين: كلمة مرور PostgreSQL، وكلمة مرور المدير المؤقت الذي ينشئه Keycloak في أول تشغيل، وسوف نعود إليه بعد قليل.

### ملف Dockerfile للنسخة المبنية مسبقاً

```dockerfile
FROM quay.io/keycloak/keycloak:26.8.1 AS builder
ENV KC_DB=postgres
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:26.8.1
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
```

```bash
docker build -t keycloak-custom:26.8.1 .
```

واستغرق البناء 83 ثانية، ولاحظ أننا ثبتنا رقم الإصدار `26.8.1` في السطرين، والسبب أن الوسم `latest` يجعل كل بناء جديد ينزل إصداراً مختلفاً دون أن تقرر أنت ذلك، وترقية Keycloak تعدل جداول قاعدة البيانات ولا رجوع عنها إلا من نسخة احتياطية Backup.

### ملف docker-compose.yml

```yaml
name: keycloak
services:
  postgres:
    image: postgres:17.6-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: ${PG_PASS:?}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
      interval: 10s
      timeout: 5s
      retries: 10
    volumes:
      - db:/var/lib/postgresql/data

  keycloak:
    image: keycloak-custom:26.8.1
    restart: unless-stopped
    command: start --optimized
    mem_limit: 768m
    healthcheck:
      test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/9000 && printf 'GET /health/ready HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 && grep -q 'HTTP/1.1 200' <&3"]
      interval: 15s
      timeout: 5s
      retries: 20
      start_period: 60s
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: ${PG_PASS}
      KC_HOSTNAME: http://localhost:18815
      KC_HTTP_ENABLED: "true"
      KC_HEALTH_ENABLED: "true"
      KC_BOOTSTRAP_ADMIN_USERNAME: tmpadmin
      KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASS:?}
    ports:
      - "127.0.0.1:18815:8080"
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  db:
```

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

- `KC_HOSTNAME` هو [العنوان العام](https://www.keycloak.org/server/hostname?ref=arabroot.io) الذي يبني منه Keycloak كل روابطه، ومنها حقل `iss` في التوكنات، وفي المختبر هو `http://localhost:18815`، وفي بيئة الإنتاج يكون `https://auth.example.com` كما في قسم الـ Reverse Proxy. وإذا غيرته بعد ربط التطبيقات فسوف ترفض التطبيقات كل التوكنات، لأن الـ Issuer لم يعد يطابق ما حفظته.
- `KC_HTTP_ENABLED` يسمح بـ HTTP داخل الشبكة، والسبب أن الـ Reverse Proxy هو الذي ينهي TLS، ولكن لا تنشر المنفذ 8080 للإنترنت أبداً، لذلك ربطناه بالعنوان `127.0.0.1`.
- الـ Image الرسمية لـ Keycloak لا تضم `curl` ولا `wget`، لذلك يفتح فحص الصحة Health Check اتصالاً عبر `/dev/tcp` في bash إلى منفذ الإدارة 9000، وهو منفذ [نقاط الفحص](https://www.keycloak.org/server/health?ref=arabroot.io) `/health/ready`، ولا يحتاج إلى نشر.
- `mem_limit: 768m` حد للذاكرة، والتفصيل في قسم الموارد، وهو مهم جداً مع Java.

### التشغيل ومتابعة السجلات Logs

```bash
docker compose up -d
docker compose logs -f keycloak
```

وفي التشغيل الأول سوف ترى في السجل سطرين مهمين، الأول يقول إن Keycloak بدأ، والثاني يقول إن التهيئة Bootstrap انتهت، والمخرج سوف يكون كما يلي:

```text
INFO  [io.quarkus] (main) Keycloak 26.8.1 on JVM (powered by Quarkus 3.40.1) started in 16.770s. Listening on: http://0.0.0.0:8080. Management interface listening on http://0.0.0.0:9000.
INFO  [org.keycloak.services.resources.KeycloakApplication] (executor-thread-1) Bootstrap completed in 9.272000 seconds
```

ولاحظ أن Keycloak في الإصدارات الحديثة يفتح المنفذ قبل أن تنتهي التهيئة، ويرد على كل طلب خلالها بالرمز 503 ورسالة `Bootstrap in progress. Retry in 1 seconds.`، وفي أول تشغيل على قاعدة بيانات فارغة تطول هذه المرحلة لأنه ينشئ الجداول، لذلك اعتمد على حالة `healthy` وليس على ظهور سطر `Listening on`:

```bash
docker compose ps
```

### المدير المؤقت ونطاق master

المتغيران `KC_BOOTSTRAP_ADMIN_USERNAME` و`KC_BOOTSTRAP_ADMIN_PASSWORD` ينشئان مديراً مؤقتاً Temporary Admin في نطاق `master` عند أول تشغيل فقط، ويظهر في السجل السطر `Created temporary admin user with username tmpadmin`، وعندما تدخل به إلى لوحة الإدارة على `http://localhost:18815/admin/` سوف ترى شريطاً أصفر يقول: «You are logged in as a temporary admin user. To harden security, create a permanent admin account and delete the temporary one». والمطلوب كما يقول الشريط: أنشئ في نطاق `master` حساب مدير باسمك، وأعطه الدور `admin`، وفعل له المصادقة الثنائية، ثم ادخل به واحذف `tmpadmin`، واحذف المتغيرين من ملف Compose. وإذا فقدت كل حسابات المديرين لاحقاً فهناك [أمر لإنشاء مدير مؤقت جديد](https://www.keycloak.org/server/bootstrap-admin-recovery?ref=arabroot.io) من سطر الأوامر.

## إنشاء نطاق للمؤسسة

تستطيع أن تنفذ كل الخطوات التالية من لوحة الإدارة، ولكننا سوف نكتبها بأداة `kcadm.sh` الموجودة داخل الـ Container، والسبب أن الأوامر تحفظ في ملف وتعيد بناء نفس الإعداد على سيرفر الاختبار أو بعد كارثة، أما النقر في الواجهة فلا يتذكره أحد. نبدأ بتسجيل الدخول بالمدير:

```bash
source .env
K="docker compose exec -T keycloak /opt/keycloak/bin/kcadm.sh"
$K config credentials --server http://localhost:8080 --realm master --user tmpadmin --password "$KC_ADMIN_PASS"
```

ثم ننشئ النطاق `arabroot` ونفعل فيه تعدد اللغات Internationalization مع العربية لغة افتراضية:

```bash
$K create realms -s realm=arabroot -s enabled=true -s displayName="عرب رووت" \
  -s internationalizationEnabled=true -s 'supportedLocales=["ar","en"]' -s defaultLocale=ar
```

```text
Created new realm with id 'arabroot'
```

ثم مجموعة `engineering` ومستخدماً فيها:

```bash
$K create groups -r arabroot -s name=engineering
$K create users -r arabroot -s username=alice -s email=alice@example.com -s emailVerified=true \
  -s firstName=Alice -s lastName=Admin -s enabled=true
$K set-password -r arabroot --username alice --new-password "$ALICE_PASS"
UID_=$($K get users -r arabroot -q username=alice --fields id --format csv --noquotes)
GID_=$($K get groups -r arabroot --fields id,name --format csv --noquotes | grep engineering | cut -d, -f1)
$K update users/$UID_/groups/$GID_ -r arabroot -s realm=arabroot -s userId=$UID_ -s groupId=$GID_ -n
```

```text
Created new group with id '869c14a3-624f-4578-ad9d-0d447deb7b3f'
Created new user with id '8dd77b51-47ae-44be-b4f0-64c447b186a4'
```

والمتغير `ALICE_PASS` كلمة مرور من 12 حرفاً أو أكثر تولدها وتعطيها للمستخدم عبر قناة آمنة، ولاحظ الخيار `-T` في `docker compose exec`، والسبب أنه يمنع إضافة رمز نهاية السطر الخاص بالطرفية إلى المعرفات IDs التي نحفظها في المتغيرات. ولاحظ أيضاً أن كل استدعاء لـ `kcadm.sh` يشغل Java من جديد، لذلك يستغرق الأمر الواحد عدة ثوان، وهذا طبيعي.

### سياسة كلمات المرور والحماية من التخمين

النطاق الجديد يأتي بلا [سياسة لكلمات المرور](https://www.keycloak.org/docs/latest/server%5Fadmin/index.html?ref=arabroot.io#%5Fpassword-policies)، وفيه [الحماية من التخمين Brute Force Detection](https://www.keycloak.org/docs/latest/server%5Fadmin/index.html?ref=arabroot.io#password-guess-brute-force-attacks) معطلة، ولا يحفظ أحداث الدخول Events في قاعدة البيانات، وقد تحققنا من ذلك على النطاق الذي أنشأناه للتو:

```bash
$K get realms/arabroot --fields bruteForceProtected,failureFactor,eventsEnabled,passwordPolicy
```

```json
{
  "bruteForceProtected" : false,
  "failureFactor" : 30,
  "eventsEnabled" : false
}
```

وهذا يعني أن أي شخص يستطيع تجربة كلمات مرور بلا حد على أي حساب، ولا يبقى أثر لذلك إلا في سجل الـ Container. لذلك فعل الثلاثة معاً، حيث نجعل الحساب يتوقف مؤقتاً بعد خمس محاولات خاطئة، ونطلب كلمة مرور من 12 حرفاً على الأقل لا تساوي اسم المستخدم ولا تكرر آخر ثلاث كلمات:

```bash
$K update realms/arabroot -s bruteForceProtected=true -s failureFactor=5 -s eventsEnabled=true \
  -s 'passwordPolicy=length(12) and notUsername and passwordHistory(3)'
```

وسوف نرى أثر هذا الإعداد في التجربة بعد قليل.

## ربط تطبيق عبر OIDC

الآن سوف نقوم بتسجيل التطبيق التجريبي `demo-app` تطبيقاً سرياً Confidential Client، أي تطبيقاً يعمل على السيرفر ويستطيع حفظ سر، ونفرض عليه PKCE بالطريقة `S256`، ونسمح بعنوان عودة واحد فقط:

```bash
CID=$($K create clients -r arabroot -s clientId=demo-app -s enabled=true -s publicClient=false \
  -s standardFlowEnabled=true -s directAccessGrantsEnabled=false \
  -s 'redirectUris=["http://localhost:18816/callback"]' -s 'webOrigins=["http://localhost:18816"]' \
  -s 'attributes={"pkce.code.challenge.method":"S256","post.logout.redirect.uris":"http://localhost:18816/"}' -i)
```

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

- `directAccessGrantsEnabled=false` يعطل تدفق كلمة المرور المباشر Password Grant، وهو تدفق [انتهى زمنه](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/#deprecated) لأن التطبيق يرى فيه كلمة مرور المستخدم.
- عنوان العودة كامل وبلا نجمة `*`، والسبب أن النجمة تسمح بإرسال الكود Authorization Code إلى أي مسار في التطبيق، ومنها مسار قد يتحكم فيه مخترق.

ولا يرسل Keycloak مجموعات المستخدم في التوكن إلا إذا طلبت ذلك، لذلك نضيف ما يسمى [Protocol Mapper](https://www.keycloak.org/docs/latest/server%5Fadmin/index.html?ref=arabroot.io#%5Fprotocol-mappers) من نوع Group Membership، ثم نقرأ سر التطبيق لنعطيه للتطبيق:

```bash
$K create clients/$CID/protocol-mappers/models -r arabroot -s name=groups -s protocol=openid-connect \
  -s protocolMapper=oidc-group-membership-mapper \
  -s 'config={"claim.name":"groups","full.path":"false","id.token.claim":"true","access.token.claim":"true","userinfo.token.claim":"true"}'
$K get clients/$CID/client-secret -r arabroot --fields value --format csv --noquotes
```

![صفحة إعداد التطبيق demo-app في لوحة إدارة Keycloak تعرض Client ID وعنوان العودة المسموح](https://arabroot.io/content/images/2026/10/keycloak-02-client-settings.webp)

التطبيق demo-app في لوحة الإدارة بعد إنشائه من سطر الأوامر

### التطبيق التجريبي

حتى نرى ما يحدث فعلاً بين التطبيق وKeycloak، كتبنا تطبيقاً صغيراً بـ Node.js لا يستخدم أي مكتبة، ينفذ تدفق Authorization Code مع PKCE، ويتحقق من `nonce` و`iss` و`aud` في الـ ID Token، ثم يعرض الحقول Claims التي وصلته. وفي تطبيقك الحقيقي استخدم مكتبة OIDC معروفة للغتك بدل هذا الكود، والسبب أن المكتبة تتحقق من توقيع التوكن عبر JWKS، وهذا ما لا يفعله المثال لأنه يأخذ التوكن مباشرة من Keycloak عبر القناة الخلفية Back Channel. والملف `demo/server.js` كما يلي:

```javascript
// Demo app for the Keycloak lab: OIDC Authorization Code + PKCE with Node's standard library only.
// ISSUER is the public address the browser sees; INTERNAL is how the container reaches Keycloak.
const http = require('node:http');
const crypto = require('node:crypto');

const ISSUER = process.env.ISSUER;            // e.g. http://localhost:18815/realms/arabroot
const INTERNAL = process.env.INTERNAL || ISSUER;
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const APP_URL = process.env.APP_URL;          // e.g. http://localhost:18816
const pending = new Map();                    // state -> { verifier, nonce }
const sessions = new Map();                   // sid -> { claims, idToken }

const b64url = (b) => Buffer.from(b).toString('base64url');
const page = (body) => `<!doctype html><html lang="ar" dir="rtl"><meta charset="utf-8"><title>تطبيق تجريبي</title>
<style>body{font:16px system-ui;margin:40px;max-width:760px}pre{direction:ltr;text-align:left;background:#f4f4f6;padding:16px;border-radius:8px}
a.btn{display:inline-block;background:#E95420;color:#fff;padding:10px 18px;border-radius:8px;text-decoration:none}</style>${body}</html>`;
const cookie = (req, name) => (req.headers.cookie || '').split(/; */).map(c => c.split('=')).find(([k]) => k === name)?.[1];

http.createServer(async (req, res) => {
  const url = new URL(req.url, APP_URL);
  if (url.pathname === '/login') {
    const state = b64url(crypto.randomBytes(16)), nonce = b64url(crypto.randomBytes(16));
    const verifier = b64url(crypto.randomBytes(32));
    const challenge = b64url(crypto.createHash('sha256').update(verifier).digest());
    pending.set(state, { verifier, nonce });
    const q = new URLSearchParams({ response_type: 'code', client_id: CLIENT_ID, redirect_uri: `${APP_URL}/callback`,
      scope: 'openid profile email', state, nonce, code_challenge: challenge, code_challenge_method: 'S256' });
    res.writeHead(302, { Location: `${ISSUER}/protocol/openid-connect/auth?${q}` }); return res.end();
  }
  if (url.pathname === '/callback') {
    const p = pending.get(url.searchParams.get('state'));
    if (!p) { res.writeHead(400); return res.end('unknown state'); }
    pending.delete(url.searchParams.get('state'));
    const r = await fetch(`${INTERNAL}/protocol/openid-connect/token`, { method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: 'Basic ' + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64') },
      body: new URLSearchParams({ grant_type: 'authorization_code', code: url.searchParams.get('code'), redirect_uri: `${APP_URL}/callback`, code_verifier: p.verifier }) });
    const t = await r.json();
    if (!t.id_token) { res.writeHead(502); return res.end(JSON.stringify(t)); }
    const claims = JSON.parse(Buffer.from(t.id_token.split('.')[1], 'base64url'));
    if (claims.nonce !== p.nonce || claims.iss !== ISSUER || claims.aud !== CLIENT_ID) { res.writeHead(401); return res.end('bad id token'); }
    const sid = b64url(crypto.randomBytes(16));
    sessions.set(sid, { claims, idToken: t.id_token });
    console.log(`login ${claims.preferred_username} acr=${claims.acr} amr=${JSON.stringify(claims.amr || null)}`);
    res.writeHead(302, { 'Set-Cookie': `sid=${sid}; HttpOnly; SameSite=Lax; Path=/`, Location: '/' }); return res.end();
  }
  const s = sessions.get(cookie(req, 'sid'));
  if (url.pathname === '/logout' && s) {
    sessions.delete(cookie(req, 'sid'));
    const q = new URLSearchParams({ id_token_hint: s.idToken, post_logout_redirect_uri: `${APP_URL}/` });
    res.writeHead(302, { Location: `${ISSUER}/protocol/openid-connect/logout?${q}` }); return res.end();
  }
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  if (!s) return res.end(page('<h1>تطبيق تجريبي</h1><p>لم تسجل الدخول بعد.</p><a class="btn" id="login" href="/login">الدخول عبر Keycloak</a>'));
  const { claims } = s;
  res.end(page(`<h1>أهلاً ${claims.name || claims.preferred_username}</h1><p>هذه الحقول Claims وصلت في الـ ID Token:</p>
<pre id="claims">${JSON.stringify({ iss: claims.iss, sub: claims.sub, preferred_username: claims.preferred_username, email: claims.email,
    groups: claims.groups, acr: claims.acr, amr: claims.amr, auth_time: claims.auth_time }, null, 2)}</pre><a class="btn" href="/logout">تسجيل الخروج</a>`));
}).listen(3000, () => console.log('demo app on :3000'));
```

ونضيف التطبيق إلى ملف Compose نفسه حتى يصل إلى Keycloak باسم الخدمة `keycloak` داخل الشبكة، أما المتصفح فيصل إليه عبر `localhost:18815`، ولهذا يحمل التطبيق عنوانين:

```yaml
  demo:
    image: node:22.23.4-alpine
    restart: unless-stopped
    working_dir: /app
    command: node server.js
    environment:
      ISSUER: http://localhost:18815/realms/arabroot
      INTERNAL: http://keycloak:8080/realms/arabroot
      CLIENT_ID: demo-app
      CLIENT_SECRET: ${DEMO_SECRET:-unset}
      APP_URL: http://localhost:18816
    volumes:
      - ./demo:/app:ro
    ports:
      - "127.0.0.1:18816:3000"
```

ضع السر الذي قرأته في `.env` باسم `DEMO_SECRET` ثم شغل الخدمة بالأمر `docker compose up -d demo`. ولاحظ أن الـ Issuer الذي يتحقق منه التطبيق هو العنوان العام وليس الداخلي، والسبب أن Keycloak يكتب في حقل `iss` قيمة `KC_HOSTNAME` مهما كان العنوان الذي وصل منه الطلب.

## صفحة الدخول بالعربية واتجاه RTL

عندما فتحنا التطبيق وضغطنا زر الدخول حولنا إلى صفحة Keycloak بالعربية مباشرة، لأن العربية هي اللغة الافتراضية للنطاق، وفي الصفحة قائمة للتبديل بين العربية والإنجليزية. والقالب الافتراضي `keycloak.v2` يضع `dir="rtl"` على الصفحة كلها عندما تكون [تعدد اللغات مفعلاً](https://www.keycloak.org/docs/latest/server%5Fadmin/index.html?ref=arabroot.io#enabling-internationalization)، لذلك جاءت الحقول والأزرار في اتجاهها الصحيح دون أي تعديل:

![صفحة دخول Keycloak باللغة العربية من اليمين إلى اليسار مع اسم النطاق عرب رووت](https://arabroot.io/content/images/2026/10/keycloak-04-login-ar.webp)

صفحة الدخول بالعربية في القالب الافتراضي keycloak.v2

ولكن الترجمة ليست كاملة، فملف رسائل صفحة الدخول العربي في الإصدار 26.8.1 ينقصه 91 مفتاحاً من أصل 525 في الملف الإنجليزي، أي أن هذه الرسائل سوف تظهر للمستخدم بالإنجليزية، ومنها رسائل التحقق من البريد وإعادة الإرسال، وفي بعض الجمل الموجودة أخطاء لغوية بسيطة مثل «رمز التحقق ذات الاستخدام الواحد». والحل لا يحتاج إلى قالب Theme جديد، وإنما تعدل النص من **Realm settings ← Localization ← Realm overrides**، حيث تختار اللغة والمفتاح وتكتب النص الذي تريده، ويحفظ في قاعدة البيانات فلا يضيع عند التحديث. أما إذا أردت شعار مؤسستك وألوانها فهنا تحتاج إلى قالب خاص، وطريقة بنائه في [دليل المطورين](https://www.keycloak.org/docs/latest/server%5Fdevelopment/index.html?ref=arabroot.io).

## فرض المصادقة الثنائية على الجميع

يأتي تدفق الدخول الافتراضي `browser` بخطوة مشروطة اسمها `Browser - Conditional 2FA`، وهذه الخطوة تطلب رمز TOTP فقط من المستخدم الذي أعد الرمز لنفسه، أما من لم يعده فيدخل بكلمة المرور وحدها. لذلك قد تظن أنك فعلت المصادقة الثنائية لأنها تعمل في حسابك، وفي الواقع فإن كل موظف لم يفتح صفحة حسابه ويعدها يدخل بعامل واحد.

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

```text
WARN  [org.keycloak.services] (executor-thread-1) KC-SERVICES0013: Failed authentication: org.keycloak.authentication.AuthenticationFlowException
WARN  [org.keycloak.events] (executor-thread-1) type="LOGIN_ERROR", realmName="arabroot", clientId="demo-app", userId="null", error="invalid_user_credentials"
```

والسبب كان في ترتيب الخطوات وليس في المتطلب نفسه، حيث عدلنا المتطلب عبر واجهة REST الإدارية Admin REST API دون أن نرسل حقل `priority`، فأعاده Keycloak إلى صفر، وبالتالي صارت خطوة الرمز قبل خطوة كلمة المرور، وطلب Keycloak رمزاً من مستخدم لا يعرفه بعد. لذلك إذا عدلت التدفقات بالأوامر فأرسل الأولوية مع كل تعديل، أو راجع الترتيب في الواجهة بعده.

والطريقة التي [يشرحها التوثيق](https://www.keycloak.org/docs/latest/server%5Fadmin/index.html?ref=arabroot.io#%5Fconditional-2fa-otp-default) هي أن تنسخ التدفق، وتضيف بعد الخطوة المشروطة خطوة ثانية تقول: إذا لم تنفذ خطوة 2FA، أي أن المستخدم لم يعد أي عامل ثان، فاطلب منه إعداد TOTP الآن. وننفذ ذلك كما يلي، مع الأولوية في كل سطر:

```bash
$K create authentication/flows/browser/copy -r arabroot -s newName=browser-mfa
$K create "authentication/flows/browser-mfa%20forms/executions/flow" -r arabroot \
  -b '{"alias":"otp-default","type":"basic-flow","provider":"basic-flow","description":"Force OTP setup when no 2FA was used"}'
$K create "authentication/flows/otp-default/executions/execution" -r arabroot -b '{"provider":"conditional-sub-flow-executed"}'
$K create "authentication/flows/otp-default/executions/execution" -r arabroot -b '{"provider":"auth-otp-form"}'
$K get authentication/flows/browser-mfa/executions -r arabroot --fields id,displayName --format csv --noquotes
```

ثم نضبط المتطلب والأولوية لكل خطوة بالمعرفات IDs التي ظهرت في المخرج، ونعرف الشرط بأن يكون صحيحاً عندما لا تنفذ خطوة 2FA، ثم نجعل التدفق الجديد هو تدفق المتصفح للنطاق:

```bash
up(){ $K update authentication/flows/browser-mfa/executions -r arabroot -b "{\"id\":\"$1\",\"requirement\":\"$2\",\"priority\":$3}"; }
up <Username Password Form> REQUIRED 10
up <browser-mfa Browser - Conditional 2FA> CONDITIONAL 20
up <otp-default> CONDITIONAL 30
up <Condition - sub-flow executed> REQUIRED 10
up <OTP Form في otp-default> REQUIRED 20
$K create authentication/executions/<Condition - sub-flow executed>/config -r arabroot \
  -b '{"alias":"no-2fa-yet","config":{"flow_to_check":"browser-mfa Browser - Conditional 2FA","check_result":"not-executed"}}'
$K update realms/arabroot -s browserFlow=browser-mfa
```

![التدفق browser-mfa في لوحة إدارة Keycloak: كلمة المرور ثم الخطوة المشروطة 2FA ثم خطوة otp-default التي تطلب OTP إلزامياً](https://arabroot.io/content/images/2026/10/keycloak-03-browser-mfa-flow.webp)

التدفق browser-mfa: كلمة المرور، ثم 2FA لمن أعده، ثم إعداد OTP إلزامي لمن لم يعده

والآن عندما تدخل alice بكلمة مرورها، ولم تعد أي عامل ثان بعد، يحولها Keycloak إلى صفحة إعداد تطبيق المصادقة مع رمز QR، وفي أعلاها تنبيه يقول إن الإعداد مطلوب لتفعيل الحساب، ولا تستطيع تجاوزها:

![صفحة إعداد تطبيق المصادقة في Keycloak بالعربية مع رمز QR وأسماء تطبيقات FreeOTP وMicrosoft Authenticator وGoogle Authenticator](https://arabroot.io/content/images/2026/10/keycloak-05-configure-totp.webp)

إعداد TOTP إلزامي في أول دخول بعد تفعيل التدفق

وبعد إدخال الرمز من التطبيق عاد المتصفح إلى التطبيق التجريبي، وهذه الحقول التي وصلت في الـ ID Token:

![التطبيق التجريبي يعرض حقول الـ ID Token ومنها iss وsub وpreferred_username وemail وgroups وacr](https://arabroot.io/content/images/2026/10/keycloak-06-demo-claims.webp)

ما وصل إلى التطبيق بعد الدخول: الهوية والبريد والمجموعات

```json
{
  "iss": "http://localhost:18815/realms/arabroot",
  "sub": "8dd77b51-47ae-44be-b4f0-64c447b186a4",
  "preferred_username": "alice",
  "email": "alice@example.com",
  "groups": [
    "engineering"
  ],
  "acr": "1",
  "auth_time": 1792956303
}
```

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

- وصلت المجموعة `engineering` في الحقل `groups` بفضل الـ Mapper، ومن هذا الحقل يقرر التطبيق صلاحياته، وأما المستخدم الذي ليس في أي مجموعة فلا يظهر له الحقل أصلاً، وهذا ما رأيناه مع مستخدم ثان، لذلك اكتب كود التطبيق بحيث يعامل غياب الحقل على أنه قائمة فارغة.
- الحقل `acr` قيمته `1` رغم أن الدخول كان بعاملين، ولم يصل الحقل `amr` الذي يذكر طرق المصادقة، والسبب أن Keycloak لا يربط مستوى المصادقة بالعاملين إلا إذا عرفت ذلك بنفسك عبر ما يسمى ACR to LoA Mapping، لذلك لا تبن على هذا الحقل في تطبيقك قبل إعداده.
- `sub` معرف ثابت لا يتغير حتى لو غير المستخدم اسمه أو بريده، فاربط به حساب المستخدم في تطبيقك وليس بالبريد.

وعند تسجيل الخروج ثم الدخول مرة ثانية طلب Keycloak الرمز بعد كلمة المرور، فأدخلنا رمزاً خاطئاً عمداً لنرى الرسالة:

![صفحة إدخال رمز لمرة واحدة في Keycloak مع رسالة رمز التحقق غير صالح](https://arabroot.io/content/images/2026/10/keycloak-07-otp-wrong.webp)

رمز خاطئ: «رمز التحقق غير صالح.»

⚠️

لا يقبل Keycloak الرمز نفسه مرتين، لأن الخيار `otpPolicyCodeReusable` قيمته `false`، فإذا سجل المستخدم الخروج ثم الدخول خلال نفس الثلاثين ثانية فعليه أن ينتظر الرمز التالي، وهذا سلوك صحيح يحميه من إعادة استخدام رمز سرقه شخص يراقب شاشته.

### ماذا يحدث عند تخمين كلمة المرور؟

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

```bash
$K get attack-detection/brute-force/users/<user-id> -r arabroot
$K get events -r arabroot --fields type,error --format csv --noquotes
```

```text
{
  "failedLoginNotBefore" : 1792957536,
  "numFailures" : 5,
  "numTemporaryLockouts" : 1,
  "disabled" : true,
  "lastIPFailure" : "192.168.128.1",
  "lastFailure" : 1792957476678
}
LOGIN_ERROR,user_temporarily_disabled
USER_DISABLED_BY_TEMPORARY_LOCKOUT,
LOGIN_ERROR,invalid_user_credentials
LOGIN_ERROR,invalid_user_credentials
LOGIN_ERROR,invalid_user_credentials
LOGIN_ERROR,invalid_user_credentials
LOGIN_ERROR,invalid_user_credentials
```

والفرق بين `failedLoginNotBefore` ووقت آخر محاولة هو 60 ثانية، وهي مدة التوقف الأولى، وتزيد مع تكرار المحاولات حتى 15 دقيقة. ولاحظ أن الحماية مبنية على الحساب وليس على عنوان IP، لذلك يستطيع مهاجم أن يوقف حساب مدير بإدخال كلمات خاطئة عمداً، ويشرح التوثيق هذا تحت عنوان [عيوب الحماية من التخمين](https://www.keycloak.org/docs/latest/server%5Fadmin/index.html?ref=arabroot.io#downside-of-keycloak-brute-force-detection)، والحل أن تضيف أمامه حداً لعدد الطلبات Rate Limiting في الـ Reverse Proxy.

## الموارد: الذاكرة RAM والقرص Disk

قد يتساءل البعض: لماذا يقول بعض الناس إن Keycloak يحتاج إلى 2 GB من الذاكرة، ويقول آخرون إنه يعمل في نصف هذا؟ والإجابة أن Java تحجز ذاكرة بحسب ما تراه متاحاً، فالـ Image تشغل Java بالخيار `-XX:MaxRAMPercentage=70`، أي أن حجم الـ Heap يصل إلى 70٪ من ذاكرة الـ Container، فإذا لم تضع حداً فهو 70٪ من ذاكرة السيرفر كله. وهذه الأرقام التي قسناها:

| الحالة                            | ذاكرة Keycloak  | ملاحظة                                             |
| --------------------------------- | --------------- | -------------------------------------------------- |
| دون حد للذاكرة، بعد التشغيل الأول | 1.21 GiB        | على سيرفر فيه 31 GB، وانخفضت إلى 880 MiB بعد دقائق |
| دون حد، بعد إعادة التشغيل         | 705 MiB         | تزيد مع الاستخدام لأن الـ Heap مسموح له بالكثير    |
| مع mem\_limit: 768m بعد التشغيل   | 508 MiB         | الحد الأقصى للـ Heap صار 538 MiB                   |
| مع الحد نفسه بعد 500 طلب توكن     | 555 إلى 580 MiB | 20 طلباً متزامناً، 6.3 ثانية، بلا أي خطأ           |
| PostgreSQL                        | 67 إلى 80 MiB   | قاعدة البيانات 13 MB والـ Volume كله 71 MB         |

وفي اختبار التحميل أعلاه أرسلنا 500 طلب توكن من نوع Client Credentials، كل 20 طلباً معاً، فكان زمن الاستجابة الوسيط p50 هو 131 ms، و95٪ من الطلبات انتهت في أقل من 349 ms، وهذا يكفي مؤسسة بمئات الموظفين يدخلون كل صباح. لذلك ضع حداً للذاكرة دائماً، والسبب أن Keycloak دون حد يعامل السيرفر كله كأنه له، فإذا كانت معه تطبيقات أخرى فقد يدفع نظام التشغيل إلى قتل أحدها عند نفاد الذاكرة OOM Killer.

## العنوان العام مع TLS عبر Reverse Proxy

في بيئة الإنتاج يصل الموظفون إلى Keycloak عبر `https://auth.example.com`، والـ Reverse Proxy ينهي TLS ويمرر الطلب إلى المنفذ 8080، وهنا يحتاج Keycloak إلى أمرين: أن تخبره بالعنوان العام في `KC_HOSTNAME`، وأن [تسمح له بقراءة](https://www.keycloak.org/server/reverseproxy?ref=arabroot.io) الترويسات `X-Forwarded-*` التي يضيفها الـ Proxy، حتى يعرف عنوان IP الحقيقي للمستخدم، وهو العنوان الذي تقوم عليه سجلات الأحداث والحماية من التخمين. فنعدل متغيرات Keycloak كما يلي:

```yaml
      KC_HOSTNAME: https://auth.example.com
      KC_PROXY_HEADERS: xforwarded
```

ومع Caddy يكفي هذا الملف، حيث يصدر Caddy الشهادة تلقائياً ويضيف الترويسات بنفسه:

```text
auth.example.com {
	reverse_proxy keycloak:8080
}
```

وجربنا ذلك محلياً مع `tls internal` بدل الشهادة العامة، فصار الـ Issuer في ملف الاكتشاف Discovery يبدأ بـ `https`:

```bash
curl -sk https://localhost:18817/realms/arabroot/.well-known/openid-configuration
```

```text
https://localhost:18817/realms/arabroot https://localhost:18817/realms/arabroot/protocol/openid-connect/auth https://localhost:18817/realms/arabroot/protocol/openid-connect/token
```

🛑

لا تضع `KC_PROXY_HEADERS` إذا كان Keycloak يستقبل طلبات مباشرة دون Proxy، والسبب أن أي شخص يستطيع حينها إرسال ترويسة `X-Forwarded-For` مزورة فيظهر في السجلات بعنوان غير عنوانه ويتجاوز الحماية المبنية على العنوان، وهذا ما [يحذر منه التوثيق](https://www.keycloak.org/server/reverseproxy?ref=arabroot.io) صراحة. ولاحظ أيضاً أن المسار `/admin/` يبقى متاحاً عبر النطاق العام، فإذا أردت لوحة الإدارة من الشبكة الداخلية فقط فاحجبه في الـ Proxy أو استخدم `KC_HOSTNAME_ADMIN` بعنوان داخلي.

## التحقق من النجاح

- `docker compose ps` يعرض `keycloak` و`postgres` بحالة `healthy`.
- عنوان الاكتشاف `https://auth.example.com/realms/arabroot/.well-known/openid-configuration` يعيد JSON فيه `issuer` بالعنوان العام وبـ `https`.
- مستخدم جديد بلا عامل ثان يجبر على إعداد TOTP قبل أن يصل إلى التطبيق، ومستخدم أعده يطلب منه الرمز في كل دخول.
- بعد خمس كلمات مرور خاطئة ترى في **Users ← المستخدم** أنه متوقف مؤقتاً، وتظهر الأحداث في **Events**.
- الشريط الأصفر للمدير المؤقت اختفى، لأنك أنشأت مديراً دائماً وحذفت `tmpadmin`.

## النسخ الاحتياطي Backup والاستعادة Restore

كل حالة Keycloak في PostgreSQL: النطاقات والمستخدمون وكلمات المرور المشفرة بالهاش وأسرار TOTP وأسرار التطبيقات، لذلك فالنسخة الاحتياطية هي نسخة قاعدة البيانات، ويجب أن تعاملها كما تعامل كلمات المرور نفسها:

```bash
docker compose exec -T postgres pg_dump -U keycloak -Fc keycloak > keycloak-$(date +%F).dump
```

وكان حجم الملف 263 KB. ولا تكتف بأخذ النسخة، وإنما جرب استعادتها، وهذا ما فعلناه في قاعدة بيانات جديدة بجانب الأصلية، ثم عددنا المستخدمين وبيانات الدخول Credentials:

```bash
docker compose exec postgres createdb -U keycloak kc_restore
docker compose exec -T postgres pg_restore -U keycloak -d kc_restore --no-owner < keycloak-2026-10-25.dump
docker compose exec postgres psql -U keycloak -d kc_restore -tc "select r.name, count(u.id) from realm r left join user_entity u on u.realm_id=r.id group by r.name"
docker compose exec postgres psql -U keycloak -d kc_restore -tc "select type, count(*) from credential group by type"
```

```text
 arabroot |     3
 master   |     1

 otp      |     2
 password |     3
```

أما أمر [kc.sh export](https://www.keycloak.org/server/importExport?ref=arabroot.io) الذي يصدر النطاق إلى ملف JSON فهو مفيد لنقل الإعداد بين بيئة الاختبار والإنتاج، ولكن التوثيق يوصي بإيقاف كل نسخ Keycloak قبل تشغيله حتى يكون التصدير متسقاً، لذلك لا تجعله نسختك الاحتياطية اليومية.

## التحديث Upgrade إلى إصدار أحدث

1. اقرأ ملاحظات الإصدار و[دليل الترقية](https://www.keycloak.org/docs/latest/upgrading/index.html?ref=arabroot.io)، وخاصة قسم Migration Changes عند الانتقال بين إصدارات فرعية مثل 26.7 إلى 26.8.
2. خذ نسخة احتياطية بالأمر السابق، والسبب أن Keycloak يعدل الجداول في أول تشغيل للإصدار الجديد، ولا يوجد أمر للرجوع.
3. غير الرقم في سطري `FROM` في الـ Dockerfile، ثم `docker build -t keycloak-custom:26.8.2 .` وغير الـ Image في ملف Compose.
4. شغل `docker compose up -d keycloak` وتابع السجل حتى سطر `Bootstrap completed` وحالة `healthy`، ثم سجل الدخول إلى تطبيق واحد على الأقل.

وتابع صفحة التنبيهات الأمنية للمشروع، أو اشترك في إشعارات الإصدارات Releases في GitHub، والسبب ما ذكرناه في البداية: إصلاحات أمنية كثيرة تصدر في الإصدارات التصحيحية، ومنها ما يمس المصادقة الثنائية نفسها.

## مشكلات شائعة وحلولها

### الرد 503 ورسالة «Bootstrap in progress»

Keycloak ما زال ينشئ الجداول أو يجهز النطاقات، وقد يطول ذلك دقائق في أول تشغيل على قرص بطيء، فانتظر حالة `healthy` ولا تعد تشغيل الـ Container خلالها.

### كل تشغيل يبدأ بسطر «Changes detected in configuration. Updating the server image»

أنت تشغل الـ Image الرسمية بالأمر `start`، فيعيد Keycloak خطوة البناء في كل Container جديد، والحل هو الـ Image المبنية مسبقاً مع `--optimized` كما في قسم التثبيت.

### صفحة الدخول تعرض «اسم المستخدم أو كلمة مرور غير صالحة.» قبل أن تكتب شيئاً

هناك خطوة في تدفق الدخول تحتاج إلى مستخدم معروف وتأتي قبل خطوة اسم المستخدم، وغالباً بسبب تعديل عبر الـ API أعاد الأولوية إلى صفر، فافتح التدفق في **Authentication** وتأكد أن Username Password Form هي الأولى. وإذا أغلقت الدخول على نفسك فلوحة الإدارة في نطاق `master` لا تتأثر بتدفقات نطاقك.

### «Invalid parameter: redirect\_uri»

العنوان الذي يرسله التطبيق لا يطابق حرفاً بحرف أحد عناوين Valid redirect URIs، ومنه الفرق بين `http` و`https` أو الشرطة المائلة في آخره، وهذا ما شرحناه في [دليل OAuth](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/#redirect-uri).

### «HTTPS required» عند الدخول من عنوان غير localhost

النطاق يطلب HTTPS لكل الطلبات الخارجية، وهذا صحيح ولا تعطله، وإنما ضع Keycloak خلف Reverse Proxy مع TLS، وتأكد من `KC_PROXY_HEADERS` حتى يعرف Keycloak أن الطلب الأصلي كان HTTPS.

### الـ Container يتوقف ويظهر في السجل OutOfMemoryError

الحد الذي وضعته صغير على عدد المستخدمين والجلسات لديك، والـ Image تستخدم `-XX:+ExitOnOutOfMemoryError` فتتوقف بدل أن تبقى في حالة غير مستقرة، فارفع `mem_limit` إلى 1g أو أكثر وراقب الذاكرة بعدها.

## كم تدفع لو اخترت خدمة تجارية؟

Keycloak مجاني بالكامل، وتكلفته الحقيقية هي السيرفر ووقت من يديره ويحدثه، وأما الخدمات السحابية المقابلة لهوية الموظفين Workforce Identity فتسعر بالمستخدم شهرياً، وهذه أسعارها من صفحاتها الرسمية بتاريخ 25 أكتوبر 2026:

| الخدمة                                                                                                          | السعر                                                                                           | شروط                                   |
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------- |
| [Okta Workforce Identity](https://www.okta.com/pricing/?ref=arabroot.io)                                        | Starter بـ 6 دولارات، وCore Essentials بـ 14 دولاراً، وEssentials بـ 17 دولاراً للمستخدم شهرياً | تدفع سنوياً، وأقل عقد سنوي 1,500 دولار |
| [Microsoft Entra ID](https://www.microsoft.com/en-us/security/business/microsoft-entra-pricing?ref=arabroot.io) | P1 بـ 7 دولارات وP2 بـ 10 دولارات للمستخدم شهرياً                                               | اشتراك سنوي                            |

فمؤسسة بـ 100 موظف تدفع لـ Okta Starter نحو 7,200 دولار في السنة، ولـ Entra ID P1 نحو 8,400 دولار، وفي المقابل تحصل على خدمة لا تديرها ولا تحدثها بنفسك. وإذا اخترت Keycloak وأردت دعماً تجارياً فـ Red Hat تقدم نسخة مدعومة اسمها [Red Hat build of Keycloak](https://access.redhat.com/products/red-hat-build-of-keycloak?ref=arabroot.io) ضمن اشتراكاتها، ولا تنشر سعرها.

## الخلاصة

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

- Keycloak يناسبك عندما تحتاج إلى نطاقات منفصلة، أو SAML مع أنظمة قديمة، أو ربط مع LDAP وActive Directory، أو تدفقات دخول تبنيها بنفسك، أما الفريق الصغير فقد يكفيه authentik أو أخف منه.
- ابن Docker Image خاصة بالأمر `kc.sh build` وشغلها بـ `--optimized`، فينزل وقت التشغيل من دقائق إلى نحو نصف دقيقة.
- ضع حداً للذاكرة، فـ Java تأخذ 70٪ مما تراه، و768 MB كانت كافية لاختبار 500 طلب توكن دون خطأ.
- النطاق الجديد بلا حماية من التخمين وبلا سياسة لكلمات المرور وبلا حفظ للأحداث، والمصادقة الثنائية في التدفق الافتراضي اختيارية فعلياً، فعدل هذا كله قبل أن تربط أول تطبيق.
- العربية مدعومة مع RTL، ولكن ينقصها 91 رسالة في صفحة الدخول تعدلها من Realm overrides.
- تابع الإصدارات التصحيحية، فقد أصلح 26.8.1 وحده نحو عشرين ثغرة، منها تجاوز إلزام المصادقة الثنائية.

وفي مقالات قادمة بإذن الله سوف نقارن Keycloak مع authentik وAuthelia وZITADEL، ونشرح متى تختار كل واحد منها.

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

- أكتوبر 2026: كتابة الدليل واختباره على Keycloak 26.8.1 مع PostgreSQL 17.6.