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

# Pocket ID: دخول بمفاتيح المرور Passkeys فقط
- URL: https://arabroot.io/articles/pocket-id-دخول-بمفاتيح-المرور-فقط/
- Published: 2026-10-26T07:01:53.000Z
- Updated: 2026-10-26T08:02:11.000Z
- Description: نشغل Pocket ID لفريق صغير فلا تبقى كلمة مرور واحدة، ونجرب ماذا يحدث عند ضياع الهاتف أو فتح صفحة على نطاق آخر.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, الاستضافة الذاتية, Pocket ID

لنفرض أن لديك استوديو تصميم صغيراً في جدة، فيه ثمانية أشخاص، وعلى سيرفر واحد يعمل [Gitea](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-gitea-%D9%85%D8%B9-docker-compose/) لملفات المشاريع، و[Nextcloud](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-nextcloud-%D9%84%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%AA%D9%82%D9%88%D9%8A%D9%85-%D9%88%D8%AC%D9%87%D8%A7%D8%AA-%D8%A7%D9%84%D8%A7%D8%AA%D8%B5%D8%A7%D9%84/) للملفات والتقويم، وتطبيق داخلي صغير فيه عروض الأسعار وبيانات العملاء. وكل خدمة بكلمة مرور، وكما يحدث في كل فريق صغير، فإن نصف الفريق يستخدم نفس كلمة المرور في الخدمات الثلاث، والنصف الآخر يكتبها في ملاحظات الهاتف. ثم وصلت إلى إحدى المصممات رسالة تقول إن حسابها في Nextcloud سوف يغلق، ومعها رابط لصفحة دخول تشبه صفحة الاستوديو تماماً، فأدخلت كلمة مرورها ورمز التحقق من تطبيق المصادقة، ولم تنتبه إلا عندما سألها زميل لماذا تغير شعار الصفحة. وفي ذلك المساء جلس صاحب الاستوديو يسأل: هل يمكن أن نعمل دون كلمات مرور أصلاً، فلا يوجد شيء يكتبه الموظف في صفحة مزيفة؟

والإجابة نعم، وهي فكرة مفاتيح المرور Passkeys التي شرحناها في [دليل Passkeys وFIDO2](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/): مفتاح خاص يبقى في جهاز الموظف، ولا يوقع إلا لنطاق الموقع الحقيقي، فلا تنفعه الصفحة المزيفة بشيء. والأداة التي سوف نستخدمها في هذا المقال هي [Pocket ID](https://pocket-id.org/?ref=arabroot.io)، وهي مزود هوية صغير يعمل عبر OIDC، وفكرته كلها في جملة واحدة: لا توجد كلمات مرور إطلاقاً، وإنما مفاتيح مرور فقط. وسوف نتابع قصة الاستوديو من تثبيت Pocket ID إلى أصعب يوم في حياة أي نظام دخول بلا كلمات مرور، وهو اليوم الذي يضيع فيه هاتف الموظف.

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

- لماذا لم يكف رمز TOTP في القصة، وماذا تقول حادثة موثقة عن ذلك.
- ما هو Pocket ID، ورخصته، وسرعة إصداراته الأمنية، وما لا يفعله عمداً.
- التثبيت بـ Docker Compose، ومفتاح التشفير Encryption Key، وإنشاء المدير بمفتاح مرور.
- كيف جربنا مفاتيح المرور دون هاتف: المصادق الافتراضي Virtual Authenticator في Chrome، وما الذي حاكيناه بالضبط.
- ربط تطبيق عبر OIDC، وقراءة التوكن، والخطأ الذي ترك المتعاقدة تدخل إلى تطبيق داخلي.
- موظف جديد برابط لمرة واحدة، ونطاق مختلف يرفض المفتاح، وهاتف ضائع، وموظف يغادر.
- النسخ الاحتياطي ومفتاح التشفير، والتحديث، والعربية، والتكلفة، وقائمة تحقق قبل أن تطلب من الفريق حذف كلمات مروره.

[![](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/)[![](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/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/password-management-cover-cover.webp)دليلإدارة كلمات المرور في المؤسسة: من ملف Excel إلى خزنةلماذا لا يصلح ملف Excel لكلمات مرور الشركة، وكيف تنقل الحسابات المشتركة إلى خزنة بصلاحيات وسجل وخطة طوارئ.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D9%83%D9%84%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%88%D8%B1-%D9%81%D9%8A-%D8%A7%D9%84%D9%85%D8%A4%D8%B3%D8%B3%D8%A9/)

## لماذا لم يكف رمز TOTP؟

المصممة في القصة كانت تستخدم المصادقة الثنائية، ومع ذلك نجح الهجوم، والسبب أن رمز TOTP شيء يكتبه الإنسان، وكل ما يكتبه الإنسان يستطيع المهاجم أن يطلبه منه، فالصفحة المزيفة تطلب كلمة المرور ثم الرمز، وترسلهما في نفس اللحظة إلى الموقع الحقيقي، فيدخل المهاجم قبل أن تنتهي صلاحية الرمز بعد ثلاثين ثانية. وهذا ما يسمى التصيد الوسيط Adversary-in-the-Middle، وقد شرحناه مع أرقامه في [دليل 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/#otp-not-enough).

📌

في 27 أغسطس 2023 وصلت إلى أحد موظفي Retool رسالة SMS تطلب منه فتح رابط بسبب مشكلة في حسابه، فدخل إلى بوابة مزيفة فيها نموذج للمصادقة الثنائية، ثم اتصل به المهاجم منتحلاً صفة أحد موظفي تقنية المعلومات، وبحسب [ما نشرته الشركة](https://retool.com/blog/mfa-isnt-mfa?ref=arabroot.io) فقد قلد صوت موظف حقيقي، وحصل منه على رمز إضافي. ولأن تطبيق Google Authenticator عند الموظف كان يزامن الرموز مع حسابه في Google، فقد حصل المهاجم على كل رموزه، وفي 29 أغسطس أبلغت Retool 27 من عملائها السحابيين بأن حساباتهم تعرضت لوصول غير مصرح به. وكتبت الشركة في الدرس المستفاد أن مفاتيح الأمان بمعيار FIDO2 تقاوم هذا النوع من الهجمات، لأنه لا يوجد رمز يستطيع الموظف أن يعطيه للمهاجم.

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

![فريق الاستوديو بمفاتيح مرور في الهواتف والحواسيب يدخل إلى Pocket ID الذي يربطه بـ Gitea وNextcloud والتطبيق الداخلي عبر OIDC، والمتعاقدة خارج المجموعة المسموحة](https://arabroot.io/content/images/2026/10/pocket-id-01-studio-map.webp)

Pocket ID في المنتصف، ولا توجد كلمة مرور في أي مكان

## ما هو Pocket ID؟

Pocket ID مزود هوية OpenID Connect مكتوب بلغة Go مع واجهة بـ SvelteKit، ويصدر [برخصة BSD 2-Clause](https://github.com/pocket-id/pocket-id/blob/main/LICENSE?ref=arabroot.io)، وهي من أكثر الرخص تساهلاً، فتستطيع تشغيله وتعديله دون شروط نشر. ويعمل في Container واحد حجم الـ Image فيه 124 MB، وقاعدة بياناته SQLite في ملف داخل الـ Volume، ويمكن أن تكون PostgreSQL إذا أردت. وصدر الإصدار الرئيسي 2.0.0 في 2 يناير 2026، ومنذ ذلك الوقت صدر 25 إصداراً، وآخرها عند كتابة هذا الدليل [v2.19.1](https://github.com/pocket-id/pocket-id/releases/tag/v2.19.1?ref=arabroot.io) في 22 أكتوبر 2026.

وما يميز Pocket ID هو ما لا يفعله عمداً: لا توجد كلمات مرور، ولا رموز TOTP، ولا تدفقات دخول تبنيها خطوة خطوة كما في [Keycloak](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-keycloak-%D9%85%D8%B2%D9%88%D8%AF-%D8%A7%D9%84%D9%87%D9%88%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A4%D8%B3%D8%B3%D8%A7%D8%AA/) أو [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/). وإنما مستخدمون ومجموعات وتطبيقات OIDC، ومفتاح مرور أو أكثر لكل مستخدم، ورابط أو رمز لمرة واحدة للحالات التي لا يوجد فيها مفتاح. وهذه البساطة هي نقطة قوته ونقطة ضعفه في نفس الوقت: سهل على فريق صغير، ولكنه لا يناسب مؤسسة فيها موظفون بلا أجهزة حديثة، أو أنظمة قديمة لا تعرف إلا LDAP أو SAML، مع أن فيه مزامنة من LDAP إذا كان المستخدمون هناك.

وقد يبدو مشروع صغير بهذا الحجم آمناً لأنه بسيط، ولكن [صفحة التنبيهات الأمنية](https://github.com/pocket-id/pocket-id/security/advisories?ref=arabroot.io) فيها 16 تنبيهاً كلها في 2026، منها واحد Critical وخمسة High، والتنبيه الحرج في يوليو 2026 كان عن رمز تحقق من البريد يبقى صالحاً بعد تغيير البريد فيسمح بانتحال بريد محقق، وأصلحه الإصدار 2.12.0، ومن تنبيهات High واحد في أغسطس سمح بإنشاء حساب مدير يتحكم فيه المهاجم عبر هجوم CSRF وأصلحه 2.13.0، وآخر في أبريل جعل تجديد التوكن Refresh Token يتجاوز تعطيل الحساب وقيود المجموعات وأصلحه 2.6.0\. والدرس أن مشروعاً حديثاً صغيراً يجد الباحثون فيه أخطاء كثيرة في سنته الأولى، وأن من يشغله يجب أن يحدثه بانتظام، تماماً كما في المشاريع الكبيرة.

## المتطلبات

- أي سيرفر صغير، فقد استهلك Pocket ID في الخمول 24 MiB من الذاكرة، ووصل بعد كل التجارب إلى 33 MiB، وحجم البيانات كلها 4.7 MB.
- نطاق فرعي مثل `id.example.com` مع HTTPS، وهذا ليس تفضيلاً، والسبب أن [المتصفحات لا تسمح بمفاتيح المرور](https://pocket-id.org/docs/setup/installation?ref=arabroot.io) إلا على صفحات HTTPS أو على `localhost`، وهو ما استخدمناه في المختبر.
- قرار مسبق باسم هذا النطاق، لأن مفاتيح المرور تربط به كما سوف نرى في قسم النطاق، وتغييره لاحقاً يعني أن يسجل كل الموظفين مفاتيحهم من جديد.
- جهاز حديث لكل موظف يدعم مفاتيح المرور: هاتف أو حاسوب بنظام حديث، أو مفتاح أمان مثل التي شرحناها في [دليل 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/#security-keys).
- SMTP اختياري، لأن الدخول برمز يصل بالبريد معطل افتراضياً، وسوف نشرح لماذا قد تفضل أن يبقى كذلك.

## التثبيت

### مفتاح التشفير في ملف

يشفر Pocket ID مفتاح توقيع التوكنات وبعض البيانات الحساسة في قاعدة البيانات بمفتاح تعطيه له، ويقترح [ملف المثال الرسمي](https://pocket-id.org/docs/configuration/environment-variables?ref=arabroot.io) طريقتين: المتغير `ENCRYPTION_KEY` مباشرة، أو `ENCRYPTION_KEY_FILE` الذي يشير إلى ملف، ويصف الثانية بأنها الموصى بها، والسبب أن متغيرات البيئة تظهر في `docker inspect` وفي سجلات بعض الأدوات، أما الملف فلا يظهر إلا لمن يقرأ القرص:

```bash
sudo mkdir -p /opt/pocket-id/secrets && cd /opt/pocket-id
openssl rand -base64 32 > secrets/encryption_key
chmod 600 secrets/encryption_key
```

### ملف .env

```ini
APP_URL=http://localhost:18823
ENCRYPTION_KEY_FILE=/run/secrets/encryption_key
TRUST_PROXY=false
ANALYTICS_DISABLED=true
```

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

- `APP_URL` هو أهم سطر في المقال كله، فمنه يأخذ Pocket ID اسم النطاق الذي تربط به مفاتيح المرور، ويسمى معرف الطرف المعتمد Relying Party ID واختصاراً `rpId`، وفي المختبر `localhost`، وفي الإنتاج `https://id.example.com`.
- `TRUST_PROXY=false` لأن المختبر بلا Reverse Proxy، وفي الإنتاج خلف Caddy أو Nginx تجعله `true` حتى يقرأ عنوان IP الحقيقي للمستخدم من الترويسات، وهو العنوان الذي يظهر في سجل التدقيق.
- `ANALYTICS_DISABLED=true` يوقف إرسال إحصاءات مجهولة إلى المشروع، وهو اختيار شخصي.

### ملف docker-compose.yml

```yaml
services:
  pocket-id:
    image: ghcr.io/pocket-id/pocket-id:v2.19.1
    restart: unless-stopped
    env_file: .env
    ports:
      - "127.0.0.1:18823:1411"
    volumes:
      - data:/app/data
      - ./secrets/encryption_key:/run/secrets/encryption_key:ro
    healthcheck:
      test: ["CMD", "/app/pocket-id", "healthcheck"]
      interval: 30s
      timeout: 5s
      retries: 2
      start_period: 10s
volumes:
  data:
```

والفرق عن [الملف الرسمي](https://pocket-id.org/docs/setup/installation?ref=arabroot.io) في ثلاثة أشياء: ثبتنا رقم الإصدار بدل الوسم `v2` الذي يتحرك مع كل إصدار، وربطنا المنفذ بالعنوان `127.0.0.1` حتى لا يصل إليه أحد إلا عبر الـ Reverse Proxy، ووضعنا البيانات في Volume بدل مجلد في مكان التشغيل. ثم نشغله:

```bash
docker compose up -d
curl -s http://localhost:18823/.well-known/openid-configuration | head -c 120
```

```text
{"authorization_endpoint":"http://localhost:18823/authorize","authorization_response_iss_parameter_supported":true,"backchan
```

وكان جاهزاً بعد 11 ثانية من الأمر الأول، ومنها تنزيل قاعدة بيانات المواقع الجغرافية GeoLite التي يستخدمها لتقدير مكان الدخول في سجل التدقيق.

## كيف جربنا مفاتيح المرور دون هاتف؟

قبل أن نكمل يجب أن نكون واضحين في نقطة: لم نستخدم في هذا المختبر هاتفاً حقيقياً ولا مفتاح أمان، وإنما مصادقاً افتراضياً Virtual Authenticator يوفره Chrome عبر [بروتوكول أدوات المطور DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/?ref=arabroot.io)، وهو نفس ما تستخدمه فرق التطوير لاختبار WebAuthn آلياً. والمصادق الافتراضي ينفذ نفس العمليات التشفيرية التي ينفذها الجهاز الحقيقي: ينشئ زوج مفاتيح، ويحفظ المفتاح الخاص، ويوقع على التحدي، ويتحقق المتصفح من النطاق قبل أي توقيع، ولكنه يتجاوز خطوة البصمة أو رمز القفل، لأننا ضبطناه على أن المستخدم «تحقق» دائماً. والكود الذي فعل ذلك في كل متصفح فتحناه كما يلي:

```javascript
await b.send('WebAuthn.enable', { enableUI: false });
const { authenticatorId } = await b.send('WebAuthn.addVirtualAuthenticator', { options: {
  protocol: 'ctap2', transport: 'internal', hasResidentKey: true,
  hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: true } });
// later: export the keys of this "device" and load them into a new browser to act as the same device
const { credentials } = await b.send('WebAuthn.getCredentials', { authenticatorId });
```

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

## المدير الأول ومفتاح مروره

عند التثبيت الأول لا يوجد أي مستخدم، والصفحة `/setup` تنشئ المدير: اسم المستخدم والبريد والاسم، ثم صفحة إعداد مفتاح المرور:

![صفحة إعداد مفتاح المرور في Pocket ID بعد إنشاء حساب المدير مع زر Add Passkey وزر Skip for now](https://arabroot.io/content/images/2026/10/pocket-id-05-add-passkey.webp)

إعداد مفتاح المرور للمدير، ولاحظ زر «Skip for now»

وبعد الضغط على Add Passkey أنشأ المصادق الافتراضي مفتاحاً، وهذا ما حفظه، كما قرأناه من المصادق نفسه:

```text
credentials in authenticator: [ { rpId: 'localhost', resident: true, signCount: 1, userHandleLen: 48 } ]
```

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

- `rpId: 'localhost'` هو النطاق الذي ربط به المفتاح، وقد جاء من `APP_URL`، ولن يوقع المصادق بهذا المفتاح لأي نطاق آخر.
- `resident: true` يعني أن المفتاح من النوع القابل للاكتشاف Discoverable Credential، أي أن الجهاز يحفظ معه هوية المستخدم، وبالتالي لا يحتاج الموظف أن يكتب اسمه في صفحة الدخول، وإنما يضغط «Authenticate» فيعرض عليه الجهاز حساباته.
- `signCount: 1` عداد يزيد مع كل توقيع، ويستخدمه السيرفر لاكتشاف مفتاح منسوخ في بعض الأجهزة، أما المفاتيح المتزامنة بين الأجهزة فعدادها غالباً صفر دائماً.

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

![صفحة دخول Pocket ID فيها زر Authenticate فقط دون حقل لاسم المستخدم أو كلمة المرور](https://arabroot.io/content/images/2026/10/pocket-id-04-login.webp)

صفحة الدخول: لا اسم مستخدم ولا كلمة مرور، وإنما زر واحد

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

## ماذا يحفظ Pocket ID عن مفتاحك؟

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

```text
aaguid 01020304-0506-0708-0102-030405060708
attestation_type none
backup_eligible false
backup_state false
created_at 2026-10-26T02:58:40Z
credential_id SAmlW+V43RrE/9sbKC9nntxXSG/RiRPk8IcOPiCH…(44)
name New Passkey
public_key owEHAzgvIFkFID7Fz0Cynm5D7ZdEXGnfS6KC/6h9…(1764)
transport WyJpbnRlcm5hbCJd
user_id c05a0592-6faf-41b5-ac1a-14d3174df94d
```

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

- `public_key` هو المفتاح العام فقط، بصيغة COSE مرمزة بـ Base64، ويكفي للتحقق من التوقيع ولا يصلح لصنعه، لذلك فسرقة هذا الجدول لا تعطي المهاجم شيئاً يدخل به، بخلاف جدول هاشات كلمات المرور الذي يستطيع أن يخمن منه كلمات المرور الضعيفة على أجهزته.
- `aaguid` معرف لنوع المصادق، والقيمة `01020304-0506-…` قيمة تجريبية يضعها مصادق Chrome الافتراضي، وفي الواقع سوف ترى هنا معرف iCloud Keychain أو Google Password Manager أو طراز مفتاح الأمان، وبه يستطيع المدير أن يعرف نوع جهاز كل موظف.
- `attestation_type none` يعني أن Pocket ID لم يطلب شهادة من الجهاز تثبت نوعه، وهذا هو الشائع في مفاتيح المرور الاستهلاكية، فإذا كانت سياسة شركتك تطلب مفاتيح أمان معتمدة فقط فهذه أداة لا تفرض ذلك.
- `backup_eligible` و`backup_state` بقيمة `false` لأن المفتاح الافتراضي مرتبط بجهاز واحد Device-bound، أما المفتاح المتزامن عبر حساب Apple أو Google فيحمل `true`، وهذا الفرق هو الذي يحدد هل يضيع المفتاح مع الهاتف أم ينتقل إلى الهاتف الجديد تلقائياً.
- `transport` قيمته Base64 للنص `["internal"]`، أي مصادق مدمج في الجهاز وليس مفتاحاً يوصل عبر USB أو NFC.

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

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

الآن سوف نبني ما في الشكل الأول: مجموعة `team` للموظفين، والمستخدم bob فيها، والمتعاقدة eve خارجها، وتطبيق OIDC للتطبيق الداخلي. وقد نفذنا ذلك بطلبات إلى الواجهة البرمجية من داخل جلسة المدير في المتصفح، لأن Pocket ID يقبل كوكي الجلسة نفسه، وفي الاستخدام اليومي تفعل نفس الشيء من لوحة الإدارة، أو تنشئ مفتاح API من الإعدادات لسكربتات الأتمتة:

```javascript
await api('POST', '/api/user-groups', { name: 'team', friendlyName: 'فريق الاستوديو' });
await api('POST', '/api/users', { username: 'bob', email: 'bob@example.com', firstName: 'Bob', lastName: 'Designer', isAdmin: false });
await api('POST', '/api/users', { username: 'eve', email: 'eve@example.com', firstName: 'Eve', lastName: 'Contractor', isAdmin: false });
await api('PUT', `/api/user-groups/${groupId}/users`, { userIds: [bobId] });
await api('POST', '/api/oidc/clients', { name: 'demo-app', callbackURLs: ['http://localhost:18824/callback'],
  logoutCallbackURLs: ['http://localhost:18824/'], isPublic: false, pkceEnabled: true });
await api('POST', `/api/oidc/clients/${clientId}/secrets`, {});
await api('PUT', `/api/oidc/clients/${clientId}/allowed-user-groups`, { userGroupIds: [groupId] });
```

```text
group 201 {"id":"584afba3-2864-462f-bdd8-8e835828a26e","friendlyName":"فريق الاستوديو","name":"team",...
user bob 201 {"id":"3adc4677-4ea7-4509-b788-0181f281fd6d","username":"bob","email":"bob@example.com",...
user eve 201
group members 200
client 201 {"id":"c9351830-e249-4f12-88c7-0336e4f1be1f","name":"demo-app",...,"clientType":"standard",...
secret 201 id,prefix,createdAt,expiresAt,isActive,secret 32
allowed groups 200
```

ولاحظ في رد إنشاء السر أنه يحمل `prefix` و`expiresAt` و`isActive`، أي أن التطبيق الواحد يستطيع أن يملك أكثر من سر، وهذا مفيد لتدوير السر Secret Rotation دون توقف: تنشئ سراً جديداً، وتضعه في التطبيق، ثم تحذف القديم. ولاحظ أيضاً أن أول محاولة لنا كانت إلى المسار `/secret` بصيغة المفرد فكان الرد 404، والمسار الصحيح `/secrets`، لذلك إذا كتبت سكربتاً من ذاكرتك أو من مثال قديم فارجع إلى الكود أو التوثيق للإصدار الذي تشغله.

### الموظف الجديد: رابط لمرة واحدة

bob لا يملك كلمة مرور ولا مفتاحاً بعد، فكيف يدخل أول مرة؟ ينشئ المدير له رابط دخول لمرة واحدة One-time Access Link، ويرسله إليه عبر قناة يثق بها، مثل محادثة الشركة أو يدوياً في يومه الأول. وهنا واجهنا مشكلة صغيرة: أنشأنا الرمز عبر الواجهة البرمجية دون تحديد المدة، فكان طوله 6 أحرف، وعندما فتح bob الرابط ظل في صفحة «Login Code» ولم يدخل، ولا توجد أي رسالة خطأ. وقرأنا كود الصفحة فوجدنا أنها تنتظر رمزاً من 12 حرفاً عندما يكون الدخول بالبريد معطلاً، وأن واجهة الإدارة نفسها ترسل دائماً مدة صلاحية `ttl`، فأعدنا الطلب بالمدة:

```javascript
await api('POST', `/api/users/${bobId}/one-time-access-token`, { ttl: '1h' });
```

```text
token 201 length 12 {"token":"<hidden>"}
```

وبهذا الرمز عمل الرابط `http://localhost:18823/lc/<token>` مباشرة، فدخل bob وانتقل إلى صفحة إعداد مفتاح المرور، فأنشأ مفتاحه على «جهازه»، أي على مصادقه الافتراضي الخاص به. ومنذ هذه اللحظة لم يعد الرابط صالحاً، لأنه لمرة واحدة، ولم يعد bob يحتاج إلى أي شيء غير جهازه. والقاعدة من هذه التجربة أن تنشئ روابط الانضمام من لوحة الإدارة، أو أن ترسل `ttl` دائماً إذا أنشأتها بسكربت، وأن تجعل المدة قصيرة، ساعة واحدة في تجربتنا، لأن الرابط يساوي الحساب كله حتى يستخدم.

### أول دخول إلى التطبيق

التطبيق التجريبي هو نفس التطبيق بلا مكتبات الذي كتبناه في [دليل Keycloak](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-keycloak-%D9%85%D8%B2%D9%88%D8%AF-%D8%A7%D9%84%D9%87%D9%88%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A4%D8%B3%D8%B3%D8%A7%D8%AA/#demo-app)، مع مسارات Pocket ID: `/authorize` و`/api/oidc/token` و`/api/oidc/end-session`، ونطاق `groups` لنقرأ المجموعات، وتعديل واحد في التحقق من الـ ID Token كان ضرورياً:

```javascript
    if (claims.nonce !== p.nonce || claims.iss !== ISSUER || ![].concat(claims.aud).includes(CLIENT_ID)) { res.writeHead(401); return res.end('bad id token'); }
```

والسبب أن أول دخول انتهى بالرسالة «bad id token» من تطبيقنا نفسه، لأن الكود القديم يقارن الحقل `aud` بنص، ومواصفة OIDC تسمح أن يكون `aud` نصاً أو قائمة، وPocket ID يرسله قائمة. وهذا خطأ شائع في الكود المكتوب يدوياً، وسبب إضافي لاستخدام مكتبة OIDC معروفة في تطبيقك الحقيقي. وبعد التعديل ضغط bob زر الدخول في التطبيق، فظهرت شاشة الموافقة Consent التي تسأله هل يشارك بريده وملفه ومجموعاته:

![شاشة الموافقة في Pocket ID تطلب من المستخدم مشاركة البريد والملف الشخصي والمجموعات مع demo-app](https://arabroot.io/content/images/2026/10/pocket-id-06-consent.webp)

شاشة الموافقة في أول دخول إلى كل تطبيق

ثم وصل إلى التطبيق، وهذه الحقول التي حملها الـ ID Token:

![التطبيق التجريبي يعرض حقول التوكن للمستخدم bob ومنها المجموعة team وطريقة المصادقة phr](https://arabroot.io/content/images/2026/10/pocket-id-07-claims.webp)

bob في مجموعة team، وطريقة الدخول phr

```json
{
  "iss": "http://localhost:18823",
  "sub": "3adc4677-4ea7-4509-b788-0181f281fd6d",
  "preferred_username": "bob",
  "email": "bob@example.com",
  "groups": [
    "team"
  ],
  "amr": [
    "phr"
  ],
  "auth_time": 1792984216
}
```

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

- `amr` قيمته `phr`، وهي اختصار Phishing-Resistant، أي «مقاوم للتصيد»، وهي من القيم المعرفة في [مواصفة OpenID EAP](https://openid.net/specs/openid-connect-eap-acr-values-1%5F0.html?ref=arabroot.io). وهذا الحقل مفيد جداً إذا كان تطبيقك يقبل الدخول من أكثر من مزود هوية، فيستطيع أن يطلب `phr` قبل العمليات الحساسة مثل تصدير بيانات العملاء.
- `groups` فيه الاسم `team` وليس الاسم الظاهر العربي، فاجعل منطق الصلاحيات في التطبيق على الاسم الثابت.
- `sub` معرف UUID ثابت، فاربط به حساب المستخدم في التطبيق، والسبب أن البريد قد يتغير، بل إن Pocket ID يسمح افتراضياً لكل مستخدم بتعديل بريده واسمه بنفسه، والإعداد اسمه `allowOwnAccountEdit` وقيمته الافتراضية `true`، فإذا ربطت الحسابات بالبريد استطاع موظف أن يغير بريده إلى بريد زميله.

وما فعلناه مع التطبيق التجريبي هو نفسه ما تفعله مع Gitea وNextcloud: تطبيق OIDC في Pocket ID لكل خدمة، بعنوان العودة الخاص بها، وسرها في إعداد الخدمة، وعنوان الاكتشاف `/.well-known/openid-configuration`، وقد شرحنا خطوات الطرف الآخر في Gitea بالتفصيل في [دليل ربط Gitea عبر OIDC](https://arabroot.io/articles/%D8%B1%D8%A8%D8%B7-gitea-%D9%85%D8%B9-authentik-%D8%B9%D8%A8%D8%B1-oidc/)، وهي نفسها مع Pocket ID بتغيير العنوان فقط. وبعد الربط عطل الدخول بكلمة المرور داخل كل خدمة، وإلا بقي باب جانبي مفتوحاً بكلمات المرور القديمة التي أردت التخلص منها، واحتفظ بحساب مدير محلي واحد في كل خدمة بكلمة مرور طويلة في خزنة الشركة، للحالة التي يتوقف فيها Pocket ID نفسه.

## المتعاقدة التي دخلت إلى التطبيق الداخلي

بعد أسبوع انضمت eve متعاقدة لمشروع واحد، فأعطاها صاحب الاستوديو حساباً حتى تدخل إلى Gitea، وقلنا في الإعداد إن التطبيق الداخلي مسموح لمجموعة `team` فقط، وeve ليست فيها. وعندما جربنا، دخلت eve إلى التطبيق الداخلي دون أي اعتراض، ووصلت إليه بمجموعات فارغة:

```json
{
  "iss": "http://localhost:18823",
  "sub": "acfe8873-6b34-423e-97ad-7ca4d2dd220b",
  "preferred_username": "eve",
  "email": "eve@example.com",
  "groups": [],
  "amr": [
    "phr"
  ],
  "auth_time": 1792984346
}
```

وقرأنا التطبيق من الواجهة البرمجية فوجدنا السبب:

```text
before: isGroupRestricted false allowedUserGroups team
```

أي أن قائمة المجموعات المسموحة محفوظة فعلاً، ولكن التقييد نفسه مفتاح منفصل اسمه `isGroupRestricted` قيمته `false`، فلا يطبق Pocket ID القائمة. وفي واجهة الإدارة يظهر هذا في تبويب Access كاختيار بين «All Users» و«Selected user groups»، أما الواجهة البرمجية فقبلت القائمة دون أن تفعل التقييد ودون أي تحذير. ثم عدلنا التطبيق فجعلنا `isGroupRestricted` بقيمة `true` وأعدنا ضبط المجموعات، لأن الكود يمسح القائمة عند كل تعديل يكون فيه التقييد معطلاً:

```text
update 200
groups 200
after: isGroupRestricted true allowedUserGroups team
```

![تبويب Access لتطبيق demo-app في لوحة إدارة Pocket ID مع اختيار Selected user groups ومجموعة فريق الاستوديو](https://arabroot.io/content/images/2026/10/pocket-id-09-admin-access.webp)

«Selected user groups» هو المفتاح الذي يفعل التقييد، وليس القائمة وحدها

وجربت eve مرة أخرى فكانت النتيجة:

![رسالة خطأ في Pocket ID تقول إن المستخدم غير مسموح له بالوصول إلى هذه الخدمة](https://arabroot.io/content/images/2026/10/pocket-id-08-not-allowed.webp)

بعد تفعيل التقييد: «You are not allowed to access this service.»

والدرس هنا أكبر من Pocket ID: كل قاعدة وصول تكتبها يجب أن تختبرها بمستخدم يجب أن يرفض، لا بمستخدم يجب أن يدخل فقط، والسبب أن الإعداد الخاطئ لا يظهر أبداً لمن له الحق في الدخول. ولو لم تكن eve في التجربة لبقي التطبيق الداخلي مفتوحاً لكل حساب في Pocket ID دون أن ينتبه أحد.

## ماذا يحدث على نطاق آخر؟

الآن نعود إلى سبب المشروع كله: الصفحة المزيفة. لا نستطيع، ولا يجوز، أن نبني موقع تصيد حقيقياً، لذلك حاكينا الفكرة بأبسط طريقة ممكنة: فتحنا Pocket ID نفسه، بنفس الـ Container ونفس قاعدة البيانات، ولكن عبر العنوان `http://127.0.0.1:18823` بدل `http://localhost:18823`، أي من أصل Origin مختلف عن النطاق الذي سجل فيه المفتاح، ثم طلب bob الدخول بمفتاحه:

![رسالة في Pocket ID تقول إن مفاتيح المرور لا يمكن استخدامها على النطاق المعرف](https://arabroot.io/content/images/2026/10/pocket-id-11-wrong-domain.webp)

نفس السيرفر ونفس المفتاح، ولكن من عنوان آخر: لا توقيع

والرسالة «Passkeys cannot be used on the configured domain» جاءت لأن المتصفح نفسه رفض الطلب قبل أن يصل إلى المصادق، والسبب أن [مواصفة WebAuthn](https://www.w3.org/TR/webauthn-3/?ref=arabroot.io#relying-party-identifier) تجعل المتصفح يتحقق أن `rpId` الذي يطلبه الموقع هو النطاق الذي في شريط العنوان أو نطاق أعلى منه، و`localhost` ليس نطاقاً للعنوان `127.0.0.1`. والشكل التالي يضع التجربة في سياق الصفحة المزيفة الحقيقية:

![خطوات الدخول بمفتاح المرور بين Pocket ID والمتصفح والهاتف، وصفحة مزيفة على نطاق مشابه يرفض فيها المتصفح طلب التوقيع](https://arabroot.io/content/images/2026/10/pocket-id-02-ceremony.webp)

المفتاح مربوط بالنطاق، والمتصفح هو الذي يتحقق، لا الموظف

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

### والوجه الآخر: لا تغير النطاق

نفس الخاصية التي تحمي من التصيد تحمي أيضاً من تغيير النطاق الذي تقرره أنت، فإذا بدأ الاستوديو بـ `id.studio.local` ثم انتقل بعد سنة إلى `id.example.com`، فإن كل مفاتيح الموظفين مربوطة بالنطاق القديم، وسوف يرى كل واحد منهم نفس الرسالة التي رآها bob على العنوان الآخر، ويحتاج كل موظف إلى رابط لمرة واحدة ومفتاح جديد. لذلك اختر النطاق النهائي من اليوم الأول، ولاحظ أن المنفذ ليس جزءاً من `rpId`، فنقل الخدمة من منفذ إلى آخر على نفس النطاق لا يمس المفاتيح.

## ضاع الهاتف

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

![رسالة في Pocket ID تقول إن طلب مفتاح المرور ألغي أو انتهت مهلته](https://arabroot.io/content/images/2026/10/pocket-id-10-new-device.webp)

جهاز بلا مفتاح: «The passkey prompt was canceled or timed out.»

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

![أربع طرق للعودة بعد ضياع الجهاز: مفتاح ثان، ورابط من المدير، وأمر سطر الأوامر للمدير الأخير، والرمز بالبريد المعطل افتراضياً، مع تحذير من التحقق من الشخص](https://arabroot.io/content/images/2026/10/pocket-id-03-recovery.webp)

جهز الطريقة الأولى يوم الانضمام، وتحقق من الشخص قبل الثانية

- **مفتاح ثان سجله مسبقاً:** Pocket ID نفسه يعرض على كل مستخدم له مفتاح واحد تحذيراً بعنوان «Single Passkey Configured» وينصحه بإضافة مفتاح آخر، وقد رأيناه عند المدير وعند bob. فاجعل تسجيل مفتاحين جزءاً من يوم الانضمام: الهاتف والحاسوب، أو الهاتف ومفتاح أمان، أو مفتاحاً متزامناً عبر حساب Apple أو Google، والفرق بينها شرحناه في [قسم المتزامن والمرتبط بجهاز](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/#synced-vs-bound).
- **رابط لمرة واحدة من المدير:** نفس طريقة الانضمام، ثم يسجل bob مفتاحاً جديداً ويحذف القديم من صفحة حسابه، حتى لا يبقى المفتاح الضائع صالحاً إذا وجده أحد وفتح الهاتف.
- **أمر سطر الأوامر:** إذا كان الذي فقد جهازه هو المدير الوحيد، فلا يوجد مدير آخر ينشئ له الرابط، وهنا يأتي الأمر الذي يعمل داخل الـ Container لمن يملك الوصول إلى السيرفر.

```bash
docker compose exec pocket-id /app/pocket-id one-time-access-token admin
```

```text
2026/10/26 03:28:23 INFO Connected to database provider=sqlite
A one-time access token valid for 1 hour has been created for "admin".
Use the following URL to sign in once: http://localhost:18823/lc/<code>
```

وهذا الأمر هو حساب الطوارئ Break-glass في Pocket ID، فمن يملك الوصول إلى السيرفر يملك كل الحسابات، وهذا منطقي لأن من يملك السيرفر يستطيع أصلاً قراءة قاعدة البيانات، ولكنه يعني أن حماية SSH إلى هذا السيرفر جزء من حماية الدخول لكل الفريق.

⚠️

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

أما الدخول برمز يصل بالبريد فهو معطل افتراضياً، وقرأنا ذلك من إعدادات النسخة: `emailOneTimeAccessAsUnauthenticatedEnabled` و`emailOneTimeAccessAsAdminEnabled` كلاهما `false`، ويحتاج تفعيله إلى SMTP. وقد يبدو مريحاً، ولكن إذا فعلته صار البريد طريقاً للدخول بجانب المفتاح، أي أن من يخترق بريد الموظف يدخل إلى كل تطبيقات الاستوديو، وبالتالي عادت الحلقة الضعيفة التي أردت إزالتها. لذلك اتركه معطلاً، إلا إذا كان بريد الشركة نفسه محمياً بمفاتيح المرور.

## الموظف الذي يغادر

في نهاية السنة غادر bob الاستوديو، وكما شرحنا في [دورة حياة حساب الموظف](https://arabroot.io/articles/%D8%AF%D9%88%D8%B1%D8%A9-%D8%AD%D9%8A%D8%A7%D8%A9-%D8%AD%D8%B3%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%88%D8%B8%D9%81-jml/) فإن الخطوة الأهم أن يغلق كل شيء في يوم مغادرته. وفي Pocket ID تعطل الحساب من صفحته، أو بالواجهة البرمجية بالحقل `disabled`، ثم حاول bob الدخول من جهازه الذي ما زال يحمل المفتاح الصحيح:

![رسالة في Pocket ID تقول إن حساب المستخدم معطل](https://arabroot.io/content/images/2026/10/pocket-id-12-disabled.webp)

المفتاح صحيح، والحساب معطل: «User account is disabled.»

ولاحظ أن تعطيل الحساب يمنع الدخول الجديد إلى Pocket ID، أما الجلسات المفتوحة في Gitea وNextcloud فتبقى حتى يطلب كل تطبيق التحقق من جديد، لذلك أغلق جلسات الموظف في كل تطبيق في نفس اليوم، أو اجعل مدة الجلسات قصيرة. وأحد التنبيهات الأمنية التي ذكرناها في البداية كان بالضبط عن تجديد التوكن الذي يتجاوز تعطيل الحساب، وقد أصلحه الإصدار 2.6.0، وهذا سبب آخر لأن لا تبقى على إصدار قديم.

## سجل التدقيق

كل دخول وكل مفتاح يضاف يسجل في سجل التدقيق Audit Log مع عنوان IP والجهاز والمكان التقريبي، ويرى كل مستخدم سجله الشخصي، ويرى المدير السجل العام:

![سجل التدقيق في Pocket ID بأحداث الدخول وإضافة مفتاح المرور مع العنوان والجهاز](https://arabroot.io/content/images/2026/10/pocket-id-13-audit.webp)

سجل التدقيق الشخصي للمدير بعد التجارب

وفي نهاية المختبر كان في السجل العام 24 حدثاً، موزعة كما يلي:

```text
audit events {"PASSKEY_ADDED":3,"SIGN_IN":13,"TOKEN_SIGN_IN":2,"NEW_CLIENT_AUTHORIZATION":2,"CLIENT_AUTHORIZATION":4} total 24
```

والحدث `TOKEN_SIGN_IN` هو الدخول برابط لمرة واحدة، وهو الحدث الذي يجب أن تراجعه بانتظام، لأن كل دخول من هذا النوع إما انضمام موظف جديد أو استعادة جهاز، فإذا وجدت حدثاً لم تنشئ له رابطاً فهذه علامة خطر. و`NEW_CLIENT_AUTHORIZATION` أول موافقة لمستخدم على تطبيق. ولاحظ أن الموقع يظهر «Internal Network» لأن الطلبات من شبكة Docker، وخلف Reverse Proxy مع `TRUST_PROXY=true` سوف يظهر العنوان الحقيقي.

## العربية

في الإصدار 2.19.1 ملفات ترجمة لـ 26 لغة، وليس بينها العربية، لذلك تظهر كل الشاشات بالإنجليزية مهما كانت لغة المتصفح، كما رأيت في كل الصور أعلاه. وهي شاشات قليلة وبسيطة، فالموظف يرى في الغالب زراً واحداً هو Authenticate، ثم شاشة الموافقة، ولكن إذا كان فريقك لا يقرأ الإنجليزية فجهز له دليلاً مصوراً من صفحة واحدة. وتستطيع أن تساهم بالترجمة العربية عبر رابط Crowdin الموجود في صفحة الإعدادات نفسها، والأسماء الظاهرة التي تكتبها أنت، مثل اسم المجموعة «فريق الاستوديو»، تظهر بالعربية بشكل صحيح.

## الموارد بالأرقام

| القياس                       | القيمة                                                     |
| ---------------------------- | ---------------------------------------------------------- |
| حجم الـ Image                | 124 MB                                                     |
| الذاكرة في الخمول            | 24 MiB                                                     |
| الذاكرة بعد كل التجارب       | 33 MiB                                                     |
| الجاهزية بعد أول تشغيل       | 11 ثانية                                                   |
| حجم مجلد البيانات            | 4.7 MB، منها قاعدة SQLite بحجم 455 KB وملف WAL بحجم 4.1 MB |
| ملف التصدير pocket-id export | 266 KB                                                     |

وهذه أصغر أرقام في هذه السلسلة، فـ Keycloak احتاج إلى أكثر من 500 MiB، وZITADEL إلى نحو 400 MiB بخدماته الأربع، وPocket ID إلى أقل من 35 MiB، لذلك يستطيع أن يعمل على نفس السيرفر الصغير الذي عليه Gitea وNextcloud دون أن تلاحظه.

## النسخ الاحتياطي ومفتاح التشفير

في Pocket ID طريقتان للنسخ: نسخ مجلد البيانات كما هو، أو الأمر `export` الذي يخرج كل شيء في ملف ZIP:

```bash
docker compose exec -T pocket-id /app/pocket-id export -p - > pocket-id-$(date +%F).zip
```

```text
Archive:  pocket-id-2026-10-26.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
    58939  1980-00-00 00:00   database.json
     8547  1980-00-00 00:00   francis.bin
   226662  1980-00-00 00:00   uploads/application-images/background.webp
```

وفيه قاعدة البيانات بصيغة JSON، وملف المفتاح المشفر، والملفات المرفوعة، ويقابله الأمر `import` للاستعادة. أما نسخ المجلد فجربناه كاملاً: أوقفنا الخدمة، وضغطنا الـ Volume في ملف بحجم 509 KB، ثم استعدناه في Volume جديد وشغلنا عليه نسخة ثانية من Pocket ID. والتجربة المهمة كانت قبل ذلك: شغلنا النسخة المستعادة بمفتاح تشفير عشوائي جديد، فرفضت أن تبدأ أصلاً:

```text
ERR Failed to run pocket-id app=pocket-id version=2.19.1 error="failed to initialize services: failed to create JWT service: error loading signing key: failed to load key: failed to decrypt key: failed to decrypt data"
```

ثم شغلناها بالمفتاح الصحيح فبدأت، وأعادت المفاتيح العامة `/.well-known/jwks.json` بالرمز 200\. وهذا سلوك أفضل مما رأيناه في [تجربة ZITADEL](https://arabroot.io/articles/zitadel-%D9%87%D9%88%D9%8A%D8%A9-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF%D8%A9-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas/#backup)، حيث بدأت النسخة بمفتاح خاطئ وبدت سليمة من الخارج، فهنا تعرف المشكلة فوراً. ولكن النتيجة واحدة: النسخة الاحتياطية دون مفتاح التشفير لا قيمة لها، فاحفظ الملف `secrets/encryption_key` في خزنة أسرار منفصلة. وإذا احتجت إلى تغيير المفتاح نفسه، مثلاً بعد مغادرة من كان يعرفه، ففي Pocket ID الأمر `encryption-key-rotate` الذي يعيد تشفير البيانات بمفتاح جديد، والأمر `key-rotate` لمفتاح توقيع التوكنات.

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

## التحديث Upgrade

1. تابع [التنبيهات الأمنية](https://github.com/pocket-id/pocket-id/security/advisories?ref=arabroot.io)، فقد رأينا 16 تنبيهاً في 2026، منها ما يمس تعطيل الحسابات وإنشاء المديرين.
2. اقرأ ملاحظات الإصدار، وخذ نسخة بالأمر `export` ومعها مفتاح التشفير.
3. غير الوسم في ملف Compose إلى الإصدار الجديد، ثم `docker compose pull && docker compose up -d`، وكان إعادة التشغيل في المختبر ثوان قليلة.
4. بعد التحديث جرب الدخول بمفتاح مستخدم عادي، ثم بمستخدم يجب أن يرفض في تطبيق مقيد بالمجموعات، كما فعلنا مع eve.

## أين يقف Pocket ID بين البدائل؟

بعد ثلاثة أشهر سأل صاحب الاستوديو سؤالاً عملياً: إذا كبر الاستوديو إلى أربعين شخصاً، أو صار له عملاء يدخلون إلى بوابة خاصة بهم، فهل يبقى Pocket ID مناسباً؟ والإجابة تعتمد على ثلاثة أشياء رأيناها في هذا المختبر وفي المقالات السابقة. الأول: هل كل المستخدمين يملكون أجهزة حديثة تدعم مفاتيح المرور؟ فإذا كان في الفريق من يعمل على حاسوب مشترك قديم، أو من يرفض أن يضع شيئاً للعمل على هاتفه، فسوف تحتاج إلى مفاتيح أمان لهم، أو إلى أداة تقبل طرقاً أخرى مثل [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/). والثاني: هل كل تطبيقاتك تعرف OIDC؟ فأداة بلا نظام دخول تحتاج إلى Forward Auth عبر الـ Reverse Proxy، وهذا ما يفعله [Authelia](https://arabroot.io/articles/authelia-%D8%AF%D8%AE%D9%88%D9%84-%D9%85%D9%88%D8%AD%D8%AF-%D9%88%D9%85%D8%B5%D8%A7%D8%AF%D9%82%D8%A9-%D8%AB%D9%86%D8%A7%D8%A6%D9%8A%D8%A9-%D8%A7%D9%85%D8%A7%D9%85-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA%D9%83/) أو authentik، والنظام القديم الذي لا يعرف إلا SAML يحتاج إلى Keycloak أو authentik. والثالث: هل لديك عملاء يجب أن يدير كل منهم مستخدميه؟ فهنا نموذج المؤسسات في [ZITADEL](https://arabroot.io/articles/zitadel-%D9%87%D9%88%D9%8A%D8%A9-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF%D8%A9-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas/) هو الأنسب. أما إذا كانت الإجابات «نعم» و«نعم» و«لا»، فإن Pocket ID يعطيك أقوى طريقة دخول متاحة اليوم بأقل تعقيد ممكن، وبذاكرة لا تلاحظها على السيرفر.

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

Pocket ID مجاني، وتكلفته السيرفر الذي يعمل عليه مع غيره، والوقت الذي تتابع فيه التحديثات. وأما الخدمات السحابية لهوية الموظفين التي تدعم مفاتيح المرور فتسعر بالمستخدم، فمثلاً تبدأ خطة Starter في [Okta Workforce Identity](https://www.okta.com/pricing/?ref=arabroot.io) بـ 6 دولارات للمستخدم شهرياً تدفع سنوياً، مع حد أدنى للعقد السنوي 1,500 دولار بحسب صفحة الأسعار بتاريخ 25 أكتوبر 2026، أي أن استوديو من ثمانية أشخاص سوف يدفع الحد الأدنى. وفي المقابل تحصل على الدعم، وعلى سياسات وصول متقدمة لا يملكها Pocket ID، مثل تقييم خطورة الجهاز والموقع قبل الدخول.

## الأخطاء الشائعة

- **قائمة مجموعات بلا تقييد:** `allowedUserGroups` لا تعمل وحدها، والمفتاح الحقيقي هو `isGroupRestricted`، أو «Selected user groups» في الواجهة.
- **مفتاح واحد لكل موظف:** ضياع الجهاز يحول الموظف إلى تذكرة دعم، فسجل مفتاحين يوم الانضمام.
- **تغيير النطاق بعد الإطلاق:** كل المفاتيح مربوطة بـ `rpId`، والنطاق الجديد يعني إعادة تسجيل الجميع.
- **إرسال رابط الاستعادة لمن اتصل:** الرابط يساوي الحساب، فتحقق من الشخص بطريقة متفق عليها.
- **تفعيل الدخول بالبريد للراحة:** يعيد البريد حلقة ضعيفة بجانب المفتاح.
- **ربط الحسابات في التطبيقات بالبريد:** المستخدم يستطيع تعديل بريده افتراضياً، فاربط بالمعرف `sub`، أو عطل `allowOwnAccountEdit`.
- **رموز لمرة واحدة بلا مدة:** أرسل `ttl` دائماً، واجعلها قصيرة.
- **مفتاح التشفير مع النسخة الاحتياطية في نفس المكان:** من يسرق الاثنين يملك مفتاح توقيع التوكنات.

## قائمة تحقق قبل أن تطلب من الفريق حذف كلمات مروره

| البند                                           | كيف تتحقق منه                                            |
| ----------------------------------------------- | -------------------------------------------------------- |
| النطاق النهائي مع HTTPS                         | APP\_URL هو النطاق الذي سيبقى سنوات                      |
| مديران على الأقل، لكل منهما مفتاحان             | صفحة المستخدمين ورسالة «Single Passkey Configured» غائبة |
| كل تطبيق داخلي مقيد بمجموعة                     | مستخدم من خارج المجموعة يرى «You are not allowed»        |
| التطبيقات تربط الحساب بـ sub                    | مراجعة إعداد OIDC في كل تطبيق                            |
| إجراء مكتوب للتحقق من الشخص قبل أي رابط استعادة | يعرفه كل من يملك صلاحية المدير                           |
| الدخول بالبريد معطل                             | الإعدادان emailOneTimeAccess... قيمتهما false            |
| مفتاح التشفير في خزنة منفصلة                    | استعادة تجريبية تبدأ وتعيد JWKS                          |
| مراجعة دورية لأحداث TOKEN\_SIGN\_IN             | كل حدث له رابط أنشأه مدير                                |
| دليل مصور قصير بالعربية للفريق                  | لأن الواجهة بالإنجليزية فقط                              |

## الخلاصة

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

- رمز TOTP يمكن أن يسرق عبر صفحة مزيفة أو مكالمة، كما حدث مع Retool، أما مفتاح المرور فيرفض المتصفح استخدامه على أي نطاق غير نطاقه، وقد رأينا ذلك في المختبر بمصادق افتراضي.
- Pocket ID مزود OIDC صغير جداً، 33 MiB من الذاكرة، يزيل كلمات المرور تماماً، ويناسب فريقاً صغيراً بأجهزة حديثة وتطبيقات تعرف OIDC.
- تقييد التطبيق بمجموعة يحتاج إلى تفعيل صريح، وقد دخلت المتعاقدة إلى التطبيق الداخلي حتى فعلناه.
- يوم ضياع الهاتف يحسم قبل وقوعه: مفتاحان لكل موظف، ورابط لمرة واحدة بعد التحقق من الشخص، وأمر سطر الأوامر للمدير الأخير.
- اختر النطاق مرة واحدة، واحفظ مفتاح التشفير بعيداً عن النسخة، وحدث بانتظام، فالمشروع الصغير وجد فيه الباحثون 16 مشكلة في سنة.

وفي المقال التالي بإذن الله سوف نقارن authentik وKeycloak وAuthelia وZITADEL، ونذكر أين يقع Pocket ID بينها.

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

- أكتوبر 2026: كتابة الدليل واختباره على Pocket ID v2.19.1 مع مصادق افتراضي في Chrome.