التعليم والبحث العلمي الأدلة التقنية

دخول موحد SSO إلى Moodle بحسابات authentik

اربط Moodle بـ authentik عبر OpenID Connect، فيدخل الطلاب والمعلمون بحساب المدرسة وتبقى الحسابات والمقررات بيد نظام الطلاب.

دخول موحد SSO إلى Moodle بحسابات authentik

في أول أسبوع من الفصل الدراسي يأتي إلى الدعم الفني سؤال واحد يتكرر مئات المرات: «نسيت كلمة مرور المنصة»، والسبب أن الطالب يحمل كلمة مرور للبريد، وأخرى لنظام الطلاب، وثالثة لمنصة التعلم Moodle، وكل واحدة منها لها سياسة مختلفة وصفحة استعادة مختلفة. والحل المعروف هو الدخول الموحد SSO، أي أن يدخل الطالب والمعلم إلى Moodle بحساب المدرسة نفسه الموجود في مزود الهوية Identity Provider، وفي دليل تثبيت Moodle ذكرنا الخطوات الأربع لذلك باختصار، ووعدنا بمقال مستقل يجربها فعلاً مع authentik، وهذا هو.

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

  • كيف تمر خطوات الدخول عبر OpenID Connect بين المتصفح وMoodle وauthentik، وأي خطوة منها تمر من السيرفر إلى السيرفر.
  • مختبر كامل على Docker فيه authentik 2026.8.4 وMoodle 5.3، والأخطاء الثلاثة التي ظهرت لنا قبل أن يعمل الربط وسبب كل منها.
  • أول دخول بالإعدادات الافتراضية، ثم الإعداد الصحيح: الأسماء العربية، واسم المستخدم، وإيقاف التأكيد بالبريد، ومنع إنشاء الحسابات من الزر.
  • تقسيم العمل بين نظام شؤون الطلاب SIS الذي ينشئ الحسابات والتسجيل في المقررات، وauthentik الذي يتحقق من الشخص.
  • جهاز المعمل المشترك: ماذا يحدث بعد الخروج، وما الذي نجح وما الذي فشل في تجربتنا لإصلاحه.

كيف يعمل الدخول الموحد SSO بين Moodle وauthentik؟

شرحنا البروتوكول نفسه في شرح OAuth وOpenID Connect للمبتدئين، فلن نعيده هنا، وما يهمنا هو أن Moodle يأتي بإضافة مصادقة OAuth 2 ضمن الكود الأساسي، وتعمل مع أي مزود يدعم OpenID Connect، وauthentik واحد منها. والشكل التالي يبين ما يحدث عندما يضغط الطالب زر الدخول:

مخطط بست خطوات: الطالب يضغط زر الدخول بحساب المدرسة في Moodle، فيحوله Moodle إلى authentik، وهناك يكتب كلمة المرور ويتحقق authentik أنه في مجموعة lms-students أو lms-teachers، ثم يعود إلى Moodle ومعه رمز مؤقت Code، ثم يطلب Moodle التوكن من authentik مباشرة عبر HTTPS، وأخيراً يطابق الحساب بالبريد ويبدأ الجلسة
خمس خطوات تمر عبر متصفح الطالب، وخطوة واحدة (5) تمر من سيرفر Moodle إلى authentik مباشرة

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

  • كلمة المرور لا تصل إلى Moodle أبداً: يكتبها الطالب في صفحة authentik، وMoodle يستلم رمزاً مؤقتاً Authorization Code يستبدله بالتوكن، لذلك لا يوجد في قاعدة بيانات Moodle ما يمكن تخمينه Cracking إذا تسربت.
  • الخطوة 5 من السيرفر إلى السيرفر: المتصفح لا يشارك فيها، فإذا لم يستطع سيرفر Moodle الوصول إلى authentik بعنوانه، أو لم يثق بشهادته، فشل الدخول حتى لو كانت صفحة authentik تفتح في المتصفح بلا مشكلة، وهذا مصدر أغلب الأخطاء كما سيأتي.
  • البوابة قبل Moodle: authentik يقرر من يحق له فتح تطبيق Moodle أصلاً بحسب المجموعات Groups، فالموظف الإداري الذي ليس طالباً ولا معلماً لا يصل إلى Moodle حتى لو كان له حساب في authentik.

ما الذي تحتاجه قبل البدء؟

  • authentik يعمل بنطاق وشهادة HTTPS: مثل https://auth.example.com، كما في تثبيت authentik للدخول الموحد. والشهادة يجب أن يثق بها سيرفر Moodle، وليس المتصفح فقط.
  • Moodle يعمل: كما في دليل تثبيت Moodle، ويفضل أن يكون بعنوان HTTPS أيضاً مثل https://lms.example.com.
  • بريد كل طالب ومعلم: فريد وصحيح في authentik وفي نظام الطلاب، لأن Moodle يربط الحسابين بالبريد كما سيأتي.
  • قرار مسبق: من ينشئ الحسابات في Moodle؟ هل هو زر الدخول نفسه، أم نظام شؤون الطلاب؟ وسوف نجرب الطريقتين ونبين لماذا نفضل الثانية.

المختبر: authentik وMoodle على Docker

الملف التالي يشغل authentik بقاعدة بياناته، وMoodle 5.3 بقاعدة بياناته، وكل المنافذ على 127.0.0.1 فقط. والحيلة الوحيدة فيه أن authentik يستمع داخل ال Container على المنفذ نفسه المنشور خارجه، أي 29143 لل HTTPS، وذلك لأن عنوان المزود Issuer يجب أن يكون واحداً في المتصفح وفي سيرفر Moodle، وفي بيئة حقيقية لا تحتاج هذه الحيلة لأن العنوان هو النطاق العام للطرفين:

name: edu3-ma
services:
  ak-db:
    image: postgres:17.11
    container_name: edu3-ma-ak-db
    environment:
      POSTGRES_DB: authentik
      POSTGRES_USER: authentik
      POSTGRES_PASSWORD: lab-ak-db-5c1e
    volumes: [ak-db:/var/lib/postgresql/data]
    healthcheck: { test: ["CMD-SHELL", "pg_isready -U authentik"], interval: 5s, retries: 30 }
    mem_limit: 512m

  authentik:
    image: ghcr.io/goauthentik/server:2026.8.4
    container_name: edu3-ma-authentik
    command: server
    environment: &ak
      AUTHENTIK_SECRET_KEY: lab-secret-key-only-for-this-test-8f3a1c9e2b7d4f6a
      AUTHENTIK_POSTGRESQL__HOST: ak-db
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: lab-ak-db-5c1e
      AUTHENTIK_BOOTSTRAP_PASSWORD: lab-ak-admin-3d9f
      AUTHENTIK_BOOTSTRAP_TOKEN: lab-ak-api-token-7b2e
      AUTHENTIK_BOOTSTRAP_EMAIL: [email protected]
      AUTHENTIK_LISTEN__HTTP: 0.0.0.0:29100
      AUTHENTIK_LISTEN__HTTPS: 0.0.0.0:29143
    ports: ["127.0.0.1:29100:29100", "127.0.0.1:29143:29143"]
    depends_on: { ak-db: { condition: service_healthy } }
    mem_limit: 1200m

  worker:
    image: ghcr.io/goauthentik/server:2026.8.4
    container_name: edu3-ma-ak-worker
    command: worker
    environment: *ak
    depends_on: { ak-db: { condition: service_healthy } }
    mem_limit: 1g

  lms-db:
    image: postgres:17.11
    container_name: edu3-ma-lms-db
    environment:
      POSTGRES_DB: moodle
      POSTGRES_USER: moodle
      POSTGRES_PASSWORD: lab-lms-db-2a7c
    volumes: [lms-db:/var/lib/postgresql/data]
    mem_limit: 512m

  moodle:
    image: moodlehq/moodle-php-apache:8.3-bookworm@sha256:501f19fa75ad3cdc8962f2a742f39d179b8aa8ffdc2ff233d8d27a32f14e0e05
    container_name: edu3-ma-moodle
    depends_on: [lms-db]
    environment:
      PHP_INI-max_input_vars: "5000"
      PHP_INI-display_errors: "Off"
    ports: ["127.0.0.1:29080:80"]
    volumes:
      - code:/var/www/html
      - data:/var/www/moodledata
      - ./moodle-router.conf:/etc/apache2/conf-enabled/moodle-router.conf:ro
      - ./tls/lab-ca.crt:/usr/local/share/ca-certificates/lab-ca.crt:ro
    mem_limit: 768m

volumes:
  ak-db: { name: edu3-ma-ak-db }
  lms-db: { name: edu3-ma-lms-db }
  code: { name: edu3-ma-code }
  data: { name: edu3-ma-data }
networks:
  default:
    name: edu3-ma-net

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

  • Moodle نفسه: وضع الكود في ال Volume، والتثبيت بالعربية من سطر الأوامر، وملف الموجه moodle-router.conf، كلها كما في دليل تثبيت Moodle حرفياً، فلن نكررها.
  • الشهادة lab-ca.crt: هي شهادة جهة إصدار CA صغيرة أنشأناها للمختبر، ووضعناها في مخزن الشهادات الموثوقة داخل Moodle، وسوف نشرح سببها في قسم الأخطاء.
  • حدود الذاكرة Memory Limits: بعد انتهاء التجارب كان سيرفر authentik يستخدم 773MB والعامل Worker يستخدم 363MB، بينما Moodle نفسه 137MB، أي أن مزود الهوية أثقل من منصة التعلم في الوضع الخامل، فخطط لذلك إذا كانت المدرسة تشغلهما على سيرفر واحد.
docker compose up -d

المجموعات والمستخدمون وتطبيق Moodle في authentik

من واجهة authentik تنشئ مزوداً Provider من نوع OAuth2/OpenID، ثم تطبيقاً Application مرتبطاً به، ثم تربط التطبيق بالمجموعات التي يحق لها فتحه، وهي الخطوات نفسها في دخول موحد إلى Gitea بحسابات authentik. ونحن نفذناها عبر واجهة authentik البرمجية API بالسكربت setup_authentik.py حتى تكون قابلة للتكرار، وهذا هو:

# Creates the lab objects in authentik through its API:
# two groups, three users, one OIDC provider and application for Moodle, bound to the two groups only.
import requests

AK = "http://edu3-ma-authentik:29100/api/v3"
S = requests.Session()
S.headers["Authorization"] = "Bearer lab-ak-api-token-7b2e"


def get(path, **params):
    r = S.get(f"{AK}{path}", params=params)
    r.raise_for_status()
    return r.json()["results"]


def post(path, body):
    r = S.post(f"{AK}{path}", json=body)
    if r.status_code >= 400:
        raise SystemExit(f"{path} {r.status_code} {r.text}")
    return r.json()


groups = {g: post("/core/groups/", {"name": g})["pk"] for g in ["lms-students", "lms-teachers"]}

USERS = [  # username, name, email, groups
    ("s2026001", "Khalid Alharbi", "[email protected]", ["lms-students"]),
    ("t.sara", "Sara Alqahtani", "[email protected]", ["lms-teachers"]),
    ("hr.omar", "Omar Alzahrani", "[email protected]", []),
]
for username, name, email, gs in USERS:
    u = post("/core/users/", {"username": username, "name": name, "email": email, "type": "internal",
                              "groups": [groups[g] for g in gs]})
    S.post(f"{AK}/core/users/{u['pk']}/set_password/", json={"password": "Lab-Pass-2026!"}).raise_for_status()
    print("user", username, "groups", gs)

scopes = {m["scope_name"]: m["pk"] for m in get("/propertymappings/provider/scope/", page_size=50)}
flow = lambda slug: get("/flows/instances/", slug=slug)[0]["pk"]
key = get("/crypto/certificatekeypairs/", name="authentik Self-signed Certificate")[0]["pk"]

prov = post("/providers/oauth2/", {
    "name": "Moodle", "client_type": "confidential",
    "client_id": "moodle", "client_secret": "lab-moodle-oidc-secret-6e4b",
    "authorization_flow": flow("default-provider-authorization-implicit-consent"),
    "invalidation_flow": flow("default-provider-invalidation-flow"),
    "grant_types": ["authorization_code", "refresh_token"],
    "redirect_uris": [{"matching_mode": "strict", "redirect_uri_type": "authorization",
                       "url": "http://127.0.0.1:29080/admin/oauth2callback.php"}],
    "property_mappings": [scopes["openid"], scopes["email"], scopes["profile"]],
    "sub_mode": "user_username", "signing_key": key})
app = post("/core/applications/", {"name": "Moodle", "slug": "moodle", "provider": prov["pk"],
                                   "policy_engine_mode": "any"})
for i, g in enumerate(groups):
    post("/policies/bindings/", {"target": app["pk"], "group": groups[g], "order": i})
print("provider", prov["pk"], "application", app["slug"], "bound to", list(groups))

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

user s2026001 groups ['lms-students']
user t.sara groups ['lms-teachers']
user hr.omar groups []
provider 1 application moodle bound to ['lms-students', 'lms-teachers']

في السكربت أعلاه لاحظ التالي:

  • عنوان الرجوع Redirect URI: هو <wwwroot>/admin/oauth2callback.php بالضبط، وبمطابقة صارمة strict، وفي الإنتاج يكون https://lms.example.com/admin/oauth2callback.php.
  • الربط بالمجموعات Policy Bindings: التطبيق مربوط بالمجموعتين lms-students وlms-teachers فقط، وتوثيق الربط يوضح أن التطبيق الذي له ربط يرفض كل من لا يطابقه، لذلك سوف يرفض authentik المستخدم hr.omar لأنه ليس في أي منهما.
  • تدفق الموافقة الضمنية Implicit Consent: لا تظهر للطالب صفحة «هل تسمح لـ Moodle بالوصول إلى بياناتك؟» في كل مرة، لأن التطبيق تطبيق المدرسة نفسها.

بعد ذلك يعطي authentik مستند الاكتشاف Discovery Document على العنوان /application/o/moodle/.well-known/openid-configuration، وهذه أهم حقوله كما ظهرت لنا:

issuer : http://edu3-ma-authentik:29100/application/o/moodle/
authorization_endpoint : http://edu3-ma-authentik:29100/application/o/authorize/
token_endpoint : http://edu3-ma-authentik:29100/application/o/token/
userinfo_endpoint : http://edu3-ma-authentik:29100/application/o/userinfo/
end_session_endpoint : http://edu3-ma-authentik:29100/application/o/moodle/end-session/
scopes_supported : ['openid', 'email', 'profile']

إضافة authentik في Moodle: ثلاثة أخطاء قبل أن يعمل

في Moodle تضيف المزود من إدارة الموقع ثم الخادم ثم خدمات OAuth 2، وتختار خدمة مخصصة Custom، وتكتب الاسم الذي يظهر على الزر وعنوان المزود Issuer URL والمعرف Client ID والسر Client Secret. ونحن استدعينا الدالة نفسها التي يستدعيها النموذج، بالسكربت create_issuer.php:

<?php
// Creates the authentik issuer in Moodle the same way the form in
// Site administration > Server > OAuth 2 services does, then prints what discovery found.
define('CLI_SCRIPT', true);
require('/var/www/html/config.php');
\core\session\manager::set_user(get_admin());

$issuer = \core\oauth2\api::create_issuer((object) [
    'name' => 'حساب المدرسة',
    'baseurl' => 'https://edu3-ma-authentik:29143/application/o/moodle/',
    'clientid' => 'moodle',
    'clientsecret' => 'lab-moodle-oidc-secret-6e4b',
    'servicetype' => 'custom',
    'image' => '',
    'showonloginpage' => \core\oauth2\issuer::LOGINONLY,
]);
echo "issuer id: {$issuer->get('id')}, requireconfirmation: {$issuer->get('requireconfirmation')}\n";
foreach (\core\oauth2\api::get_endpoints($issuer) as $e) {
    echo "endpoint {$e->get('name')}: {$e->get('url')}\n";
}
foreach (\core\oauth2\api::get_user_field_mappings($issuer) as $m) {
    echo "mapping {$m->get('externalfield')} -> {$m->get('internalfield')}\n";
}
docker cp create_issuer.php edu3-ma-moodle:/tmp/create_issuer.php
docker exec -u www-data edu3-ma-moodle php /tmp/create_issuer.php

الخطأ الأول: HTTPS فقط

في المحاولة الأولى كتبنا عنوان authentik بال HTTP، كما يفعل من يجرب على شبكة داخلية، فرفضه Moodle قبل أن يتصل به:

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

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

الخطأ الثاني: الشهادة غير موثوقة

authentik يفتح منفذ HTTPS بشهادة موقعة ذاتياً Self-signed من البداية، وعند الاتصال به من داخل Moodle كانت النتيجة:

docker exec edu3-ma-moodle curl -s -o /dev/null -w '%{http_code} ssl_verify=%{ssl_verify_result}\n' \
  https://edu3-ma-authentik:29143/application/o/moodle/.well-known/openid-configuration
000 ssl_verify=18

والرمز 18 في OpenSSL معناه شهادة موقعة ذاتياً، وال curl يرفضها. وفي الإنتاج يكون أمام authentik شهادة صحيحة من Let's Encrypt عبر Reverse Proxy كما في تثبيت Caddy، فلا تظهر هذه المشكلة، وأما في المختبر، أو في مدرسة تستخدم جهة إصدار داخلية لشبكتها، فالحل أن تصدر شهادة لاسم authentik من جهة الإصدار الداخلية، وتجعل Moodle يثق بهذه الجهة. أنشأنا الجهة والشهادة بالسكربت make-tls.sh:

mkdir -p tls && cd tls
openssl req -x509 -newkey rsa:2048 -nodes -days 30 -keyout lab-ca.key -out lab-ca.crt -subj "/CN=Lab School CA"
openssl req -newkey rsa:2048 -nodes -keyout authentik.key -out authentik.csr -subj "/CN=edu3-ma-authentik"
printf "subjectAltName=DNS:edu3-ma-authentik\nextendedKeyUsage=serverAuth\n" > ext.cnf
openssl x509 -req -in authentik.csr -CA lab-ca.crt -CAkey lab-ca.key -CAcreateserial -days 30 -out authentik.crt -extfile ext.cnf

ثم رفعنا الشهادة إلى authentik وجعلناها شهادة الواجهة Web Certificate للعلامة الافتراضية Brand، وهي التي يقدمها authentik على منفذ HTTPS، كما في توثيق الشهادات:

import requests

AK = "http://edu3-ma-authentik:29100/api/v3"
H = {"Authorization": "Bearer lab-ak-api-token-7b2e"}
kp = requests.post(f"{AK}/crypto/certificatekeypairs/", headers=H, json={
    "name": "edu3-ma-authentik",
    "certificate_data": open("tls/authentik.crt").read(),
    "key_data": open("tls/authentik.key").read()}).json()
brand = requests.get(f"{AK}/core/brands/", headers=H).json()["results"][0]
r = requests.patch(f"{AK}/core/brands/{brand['brand_uuid']}/", headers=H, json={"web_certificate": kp["pk"]})
print("certificate", kp["name"], "brand", brand["domain"], "->", r.status_code)
certificate edu3-ma-authentik brand authentik-default -> 200

وفي Moodle حدثنا مخزن الشهادات بالأمر update-ca-certificates، بعد أن ربطنا ملف الجهة في Compose كما رأيت أعلاه:

docker exec edu3-ma-moodle update-ca-certificates

ولاحظ أمرين من التجربة: الأول أن authentik بقي يقدم الشهادة القديمة حتى أعدنا تشغيل ال Container بالأمر docker restart edu3-ma-authentik، وبعدها أصبحت النتيجة 200 ssl_verify=0. والثاني أن تحديث مخزن الشهادات داخل Container يضيع عند إعادة إنشائه، لذلك في الإنتاج ابن Docker Image خاصاً بك فيه الشهادة، أو شغل الأمر في بداية تشغيل ال Container.

الخطأ الثالث: «تم حظر عنوان URL»

بعد أن أصبح العنوان HTTPS والشهادة موثوقة، فشل الاكتشاف برسالة جديدة:

PHP Notice:  Blocked https://edu3-ma-authentik:29143/application/o/moodle/.well-known/openid-configuration: تم حظر عنوان URL. [user 2]
...
!!! error/Could not discover service endpoints: تم حظر عنوان URL. !!!

والسبب أن Moodle يحمي نفسه من هجمات تزوير الطلبات من جهة السيرفر SSRF، فكل طلب يرسله السيرفر يمر على قائمتين في إعدادات الأمان: قائمة العناوين المحظورة curlsecurityblockedhosts، وقائمة المنافذ المسموحة curlsecurityallowedport. وهذه قيمهما الافتراضية كما قرأناها من الموقع:

docker exec -u www-data edu3-ma-moodle php /var/www/html/admin/cli/cfg.php --name=curlsecurityblockedhosts
docker exec -u www-data edu3-ma-moodle php /var/www/html/admin/cli/cfg.php --name=curlsecurityallowedport
127.0.0.0/8
192.168.0.0/16
10.0.0.0/8
172.16.0.0/12
0.0.0.0
localhost
169.254.169.254
0000::1
443
80

أي أن العنوان ممنوع لسببين معاً: المنفذ 29143 ليس في القائمة، وعنوان authentik داخل شبكة Docker كان في النطاق 192.168.96.0/20، وهو داخل 192.168.0.0/16 المحظور. وقد جربنا إضافة المنفذ وحده، فبقي الخطأ نفسه، ثم أزلنا النطاق 192.168.0.0/16 وحده، فنجح الاكتشاف:

docker exec -u www-data edu3-ma-moodle php /var/www/html/admin/cli/cfg.php --name=curlsecurityallowedport --set="$(printf '443\n80\n29143')"
docker exec -u www-data edu3-ma-moodle php /var/www/html/admin/cli/cfg.php --name=curlsecurityblockedhosts \
  --set="$(printf '127.0.0.0/8\n10.0.0.0/8\n172.16.0.0/12\n0.0.0.0\nlocalhost\n169.254.169.254\n0000::1')"
issuer id: 6, requireconfirmation: 1
endpoint discovery_endpoint: https://edu3-ma-authentik:29143/application/o/moodle/.well-known/openid-configuration
endpoint authorization_endpoint: https://edu3-ma-authentik:29143/application/o/authorize/
endpoint token_endpoint: https://edu3-ma-authentik:29143/application/o/token/
endpoint userinfo_endpoint: https://edu3-ma-authentik:29143/application/o/userinfo/
endpoint end_session_endpoint: https://edu3-ma-authentik:29143/application/o/moodle/end-session/
endpoint introspection_endpoint: https://edu3-ma-authentik:29143/application/o/introspect/
endpoint revocation_endpoint: https://edu3-ma-authentik:29143/application/o/revoke/
endpoint device_authorization_endpoint: https://edu3-ma-authentik:29143/application/o/device/
mapping address -> address
mapping nickname -> alternatename
mapping email -> email
mapping given_name -> firstname
mapping locale -> lang
mapping family_name -> lastname
mapping middle_name -> middlename
mapping phone -> phone1
mapping picture -> picture
🛑
لا تقم بتفريغ قائمة العناوين المحظورة كلها لتتخلص من الخطأ، والسبب أن هذه القائمة هي التي تمنع معلماً يرفع رابطاً في نشاط ما من جعل سيرفر Moodle يطلب عناوين الشبكة الداخلية أو خدمة البيانات الوصفية للسحابة 169.254.169.254. الحل الأنسب أن يصل Moodle إلى authentik بنطاقه العام وعلى المنفذ 443، فلا تحتاج أي استثناء، وإذا كان لا بد من عنوان داخلي فأزل أصغر نطاق يحتويه فقط.

لاحظ أيضاً أن الاكتشاف ملأ نقاط الاتصال كلها، وأنشأ تسعة روابط للحقول User Field Mappings من القائمة الافتراضية، وليس بينها اسم المستخدم، وسوف نرى أثر ذلك بعد قليل. وأخيراً نفعل إضافة المصادقة OAuth 2:

docker exec -u www-data edu3-ma-moodle php /var/www/html/admin/cli/cfg.php --name=auth --set=oauth2

أول دخول بالإعدادات الافتراضية

الآن تظهر في صفحة الدخول في Moodle الزر «تسجيل الدخول مع حساب المدرسة»:

صفحة الدخول في Moodle 5.3 بالعربية: مرحباً بعودتكم، وحقلا اسم المستخدم وكلمة المرور، وزر تسجيل الدخول، ثم فاصل مكتوب عليه تمام، ثم زر تسجيل الدخول مع حساب المدرسة وبجانبه أيقونة لا تظهر، وعلى اليسار صورة معمل حاسب
زر الدخول بحساب المدرسة تحت النموذج المحلي، وأيقونته فارغة لأننا لم نعطه رابط أيقونة

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

صفحة دخول authentik وفيها الشعار وعبارة Welcome to authentik وLog in to continue to Moodle، وحقل البريد الإلكتروني أو اسم المستخدم بالعربية، وزر تسجيل الدخول بالعربية، واختيار اللغة العربية في الأعلى
صفحة authentik تذكر أن الدخول من أجل Moodle، وجزء من نصوصها مترجم إلى العربية وجزء بقي بالإنجليزية

دخل الطالب s2026001 بكلمة مروره، فعاد إلى Moodle بهذه الصفحة:

صفحة في Moodle عنوانها قم بتأكيد اشتراكك: تم إرسال بريد إلى s2026001@students.example.com والذي يحتوي على إرشادات سهلة تعينك على إتمام التسجيل، إذا استمرت المشاكل اتصل بمدير هذا الموقع، وزر استمر
بالإعداد الافتراضي ينشئ Moodle حساباً معلقاً ويطلب تأكيده من البريد

وهذا ما أنشأه Moodle في قاعدة البيانات:

 id |           username            |  auth  | confirmed |   firstname    | lastname |             email
----+-------------------------------+--------+-----------+----------------+----------+-------------------------------
  3 | [email protected] | oauth2 |         0 | Khalid Alharbi |          | [email protected]

في المخرج أعلاه لاحظ التالي، فكل سطر منه مشكلة ستراها في مدرسة حقيقية:

  • الحساب غير مؤكد confirmed = 0: الخاصية «يتطلب تأكيد البريد» Require Email Verification مفعلة افتراضياً في كل مزود جديد، وقيمتها true في تعريف الكلاس. والبريد لن يصل إذا لم يكن بريد Moodle مضبوطاً، وحتى لو وصل فهو خطوة لا معنى لها، لأن authentik هو مزود الهوية الذي تديره المدرسة نفسها، والبريد فيه صحيح من البداية.
  • اسم المستخدم هو البريد: لأن القائمة الافتراضية لا تربط أي حقل باسم المستخدم، وكود المصادقة يستخدم البريد عندها، بينما المدرسة تريد الرقم الجامعي s2026001.
  • الاسم كله في الاسم الأول: authentik يحمل للمستخدم حقلاً واحداً هو الاسم الكامل، ويرسله في given_name، فأصبح «Khalid Alharbi» اسماً أول، والاسم الأخير فارغ، وهي أسماء إنجليزية بينما المدرسة تريد الاسم العربي في الشهادات وكشوف الدرجات.

الإعداد الصحيح للمدرسة

الأسماء العربية واسم المستخدم من authentik

في authentik وضعنا الاسم الأول والأخير بالعربية في خصائص المستخدم Attributes، وأنشأنا ربطاً للنطاق Scope Mapping اسمه profile يرسلها مع اسم المستخدم، واستبدلنا به الربط الافتراضي في المزود، كما في توثيق ربط الخصائص Property Mappings. وهذا تعبير الربط:

return {
    "preferred_username": request.user.username,
    "given_name": request.user.attributes.get("first_name", request.user.name),
    "family_name": request.user.attributes.get("last_name", ""),
}

وفي المختبر أضاف السكربت names_authentik.py الخصائص لثلاثة مستخدمين، وأنشأ الطالبة s2026002 في مجموعة الطلاب، وهي موجودة في authentik فقط وليست في ملف نظام الطلاب، لنرى ما يحدث لها لاحقاً. وفي Moodle أوقفنا التأكيد بالبريد، وربطنا preferred_username باسم المستخدم، وهذا ما يقابل في الواجهة إلغاء خيار Require Email Verification في الخدمة، ثم إضافة سطر في صفحة ربط حقول المستخدم User Field Mappings:

<?php
// Turns off e-mail confirmation for the authentik issuer and maps preferred_username to the Moodle username.
define('CLI_SCRIPT', true);
require('/var/www/html/config.php');
\core\session\manager::set_user(get_admin());
$issuers = \core\oauth2\api::get_all_issuers(true);
$issuer = reset($issuers);
$issuer->set('requireconfirmation', 0);
$issuer->update();
(new \core\oauth2\user_field_mapping(0, (object) [
    'issuerid' => $issuer->get('id'), 'externalfield' => 'preferred_username', 'internalfield' => 'username']))->create();
foreach (\core\oauth2\api::get_user_field_mappings($issuer) as $m) {
    echo "{$m->get('externalfield')} -> {$m->get('internalfield')}\n";
}
echo "requireconfirmation: {$issuer->get('requireconfirmation')}\n";

ولاحظ أن الدالة get_all_issuers() دون المعامل true أرجعت قائمة فارغة في تجربتنا، لأنها تستبعد المزودين المخصصين لصفحة الدخول فقط Login Only، وهو نوع مزودنا.

منع إنشاء الحسابات من زر الدخول

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

docker exec -u www-data edu3-ma-moodle php /var/www/html/admin/cli/cfg.php --name=authpreventaccountcreation --set=1

والشكل التالي يلخص تقسيم العمل الذي نريده:

ثلاثة صناديق: نظام شؤون الطلاب SIS يرسل ملف CSV فيه الطالب ورقمه ومقرراته ودوره وزمرته إلى Moodle، وMoodle يملك الحساب والمقررات والدرجات ويربط الدخول بالبريد ويرفض من ليس له حساب، وauthentik يرسل إلى Moodle عبر OIDC ويملك كلمة المرور والتحقق الثنائي والمجموعات والاسم العربي. وتحتها ثلاث حالات: في authentik وفي CSV يدخل مباشرة، وفي authentik وليس في CSV يرفضه Moodle، وليس في مجموعات Moodle يرفضه authentik
نظام الطلاب ينشئ الحساب، وauthentik يتحقق من الشخص، وMoodle يربط بينهما بالبريد

الحسابات من نظام شؤون الطلاب عبر CSV

أداة رفع المستخدمين التي استخدمناها في دليل Moodle تقبل عمود auth، فإذا كانت قيمته oauth2 أصبح الحساب حساب دخول موحد بلا كلمة مرور محلية، وعمود cohort1 يضيف المستخدم إلى زمرة Cohort، وهي مجموعة على مستوى الموقع يمكن أن تسجل في المقررات دفعة واحدة. أنشأنا مقرر CS101، ثم الملف users.csv للمعلمة:

username,auth,firstname,lastname,email,idnumber,course1,role1,cohort1
t.sara,oauth2,سارة,القحطاني,[email protected],T-0042,CS101,editingteacher,teachers-2026
docker cp users.csv edu3-ma-moodle:/tmp/
docker exec -u www-data edu3-ma-moodle php /var/www/html/public/admin/tool/uploaduser/cli/uploaduser.php \
  --file=/tmp/users.csv --uupasswordnew=1
السطر 2
  مستخدم جديد
  الانضمامات: تمت إضافة المستخدم إلى الزمرة "teachers-2026"
                        منضم إلى "CS101" بدور "editingteacher"
-------------------------------------------------------------------------------
تم إنشاء المستخدمين: 1
المستخدمون ذوو كلمة المرور الضعيفة: 0
الأخطاء: 0

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

  • في المحاولة الأولى استخدمنا --uupasswordnew=0 كما في دليل Moodle، فرفضت الأداة الملف بالرسالة «الحقل "password" غير موجود»، لأن هذا الخيار يعني أن كلمة المرور في الملف، والحل هو --uupasswordnew=1، ولم تولد الأداة كلمة مرور فعلاً لأن الحساب من نوع oauth2، فقيمة الحقل في قاعدة البيانات أصبحت not cached.
  • الزمرة teachers-2026 لم تكن موجودة، فأنشأتها الأداة وأضافت المعلمة إليها في الخطوة نفسها.
  • الرقم الوظيفي في عمود idnumber هو المفتاح الذي يربط سجل Moodle بسجل نظام الطلاب في التقارير والتصدير، فلا تتركه فارغاً.

ثلاث حالات دخول بعد الإعداد الصحيح

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

الحسابفي authentikفي ملف CSVالنتيجة
t.saraمجموعة المعلميننعمدخلت مباشرة إلى لوحة التحكم، وربط Moodle الحساب بالبريد تلقائياً
s2026002مجموعة الطلابلارفضها Moodle: «تعذر العثور على حساب بعنوان بريدك الالكتروني»
hr.omarبلا مجموعةلارفضه authentik قبل أن يصل إلى Moodle: «الصلاحية مرفوضة»

وهذا جدول الربط Linked Logins في Moodle بعد دخول المعلمة، وفيه أن الحساب الذي أنشأه نظام الطلاب ارتبط بالمستخدم t.sara القادم من authentik دون أي تدخل:

 userid | username |       email
--------+----------+--------------------
      4 | t.sara   | [email protected]

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

صفحة الدخول في Moodle وفي أعلاها رسالة حمراء: محاولة تسجيل الدخول فشلت. السبب: تعذر العثور على حساب بعنوان بريدك الالكتروني
الطالبة موجودة في authentik ولكن نظام الطلاب لم ينشئ لها حساباً، فيرفضها Moodle
صفحة authentik بعنوان الصلاحية مرفوضة، وتحتها اسم المستخدم hr.omar ورابط لست أنت، ورسالة تم رفض الطلب، وزر العودة للرئيسية
الموظف ليس في مجموعات Moodle، فيرفضه authentik ولا يصل الطلب إلى Moodle أصلاً
⚠️
الربط بالبريد يعني أن من يملك البريد يملك الحساب، لذلك لا تسمح للمستخدم بتغيير بريده في authentik بنفسه، ولا تسمح بالبريد نفسه لحسابين، والسبب أن تغيير البريد إلى بريد معلم آخر يجعل Moodle يربط الدخول الجديد بحساب ذلك المعلم.

ماذا حدث للحساب الذي أنشئ قبل الإعداد الصحيح؟

الطالب s2026001 دخل مرة قبل تغيير الإعدادات، فبقي له حساب معلق باسم مستخدم هو بريده. وبعد الإعداد الصحيح دخل مرة أخرى، فحدث التالي:

  1. تحدث اسمه في Moodle إلى «خالد» و«الحربي» من authentik.
  2. أنشأ Moodle ربطاً ثانياً للحساب نفسه باسم s2026001، بجانب الربط الأول باسم البريد.
  3. ظهرت له رسالة «لم يتمكن من تأكيد [email protected]».
  4. في الدخول الذي بعده دخل إلى لوحة التحكم بلا مشكلة، ولكن اسم المستخدم في Moodle بقي البريد، والحساب ما زال بالقيمة confirmed = 0.
 id |           username            | confirmed | firstname | lastname
----+-------------------------------+-----------+-----------+----------
  3 | [email protected] |         0 | خالد      | الحربي

 userid |           username
--------+-------------------------------
      3 | [email protected]
      3 | s2026001

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

لوحة التحكم في Moodle بالعربية للطالب خالد: مرحباً خالد، واستعراض المقررات الدراسية فارغ برسالة لست منضماً إلى أي مساق، والمخطط الزمني
الطالب دخل باسمه العربي من authentik، ولكن لا مقررات له، لأنه لم يأت من نظام الطلاب

تحديث الاسم في كل دخول

غيرنا الاسم الأخير للمعلمة في authentik إلى «آل قحطان»، ثم دخلت مرة أخرى، فأصبح في Moodle:

 username | firstname | lastname
----------+-----------+----------
 t.sara   | سارة      | آل قحطان

أي أن Moodle حدث الاسم من authentik فوق الاسم الذي جاء من ملف CSV، وهذا جيد إذا كان authentik هو مصدر الأسماء الصحيح. ولكن لاحظ أن الحقول في إعدادات إضافة OAuth 2 كلها بالقيمة unlocked افتراضياً، فيستطيع الطالب أن يغير اسمه في ملفه في Moodle، ويبقى التغيير حتى دخوله التالي. لذلك اقفل الاسم الأول والأخير والبريد من صفحة إعدادات الإضافة (Lock user fields)، حتى يكون المصدر واحداً.

جهاز المعمل المشترك: الخروج من Moodle لا يكفي

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

1 signed in : http://127.0.0.1:29080/my/
2 after logout: http://127.0.0.1:29080/login/index.php | logged in: false
3 pressed the button again: http://127.0.0.1:29080/my/

الضغط على زر حساب المدرسة بعد الخروج أعاد الطالب الأول إلى لوحة التحكم دون أن يطلب كلمة المرور، والسبب أن الخروج أنهى جلسة Moodle فقط، وأما جلسة authentik في المتصفح فما زالت حية، وauthentik يرد على طلب الدخول الجديد مباشرة. وقد قرأنا ملف الخروج login/logout.php في 5.3 فوجدناه يستدعي خطافات Hooks إضافات المصادقة ثم ينهي الجلسة، وإضافة OAuth 2 لا تطلب نقطة الخروج من المزود، رغم أن الاكتشاف حفظها باسم end_session_endpoint. وجربنا ثلاثة حلول:

ما جربناهأينالنتيجة في تجربتنا
إضافة prompt=login في معاملات الدخول Login Parametersإعدادات الخدمة في Moodleحلقة لا تنتهي: سجل authentik نجاح الدخول، ثم أعاد صفحة اسم المستخدم من جديد، لأن المعامل ينتقل مع كل طلب
إضافة max_age=0إعدادات الخدمة في Moodleلم يتغير شيء، ودخل الطالب دون كلمة مرور
تقصير مدة جلسة authentik session_duration إلى دقيقةمرحلة default-authentication-login في authentikبعد 70 ثانية من الخروج، طلب authentik اسم المستخدم من جديد

والحل الثالث هو الذي نجح، وهو إعداد مرحلة تسجيل الدخول User Login Stage، وتوثيقها يذكر أن القيمة الافتراضية seconds=0 تعني أن الجلسة تبقى حتى يغلق المتصفح. فالقاعدة لأجهزة المعامل كما يلي:

  • اجعل المتصفح في أجهزة المعامل يحذف بياناته عند الإغلاق، أو استخدم وضع الضيف Guest Mode، فتنتهي جلسة authentik مع إغلاقه.
  • قصر مدة جلسة authentik إلى مدة حصة واحدة مثل minutes=45، وتذكر أن هذا الإعداد على تدفق الدخول كله، فإذا كان تدفقاً مشتركاً بين كل تطبيقات المدرسة فهو يؤثر على الموظفين أيضاً، والأنسب تدفق دخول مستقل للطلاب.
  • علم الطلاب أن الخروج الكامل من صفحة authentik نفسها، وليس من Moodle فقط.

هل تتحول مجموعات authentik إلى أدوار في Moodle؟

لا، وهذا ما يسأل عنه الجميع بعد أن يعمل الدخول. إضافة OAuth 2 في Moodle تأخذ من authentik حقول الملف الشخصي فقط، كما في قائمة الربط أعلاه، ولا يوجد فيها ربط للمجموعات Groups بالأدوار Roles أو بالمقررات، والمجموعات في المختبر استخدمها authentik ليقرر من يدخل فقط. لذلك يبقى الدور والتسجيل في المقرر من مصدر آخر، والأنسب في مؤسسة تعليمية أن يكون نظام شؤون الطلاب كما فعلنا بملف CSV، أو عبر الزمر Cohorts وطريقة التسجيل بمزامنة الزمرة Cohort Sync، فيسجل الطلاب في المقرر بإضافتهم إلى الزمرة. وقد شرحنا فكرة توزيع الأدوار من مزود الهوية على التطبيقات التي تدعمها في إدارة الأدوار عبر تطبيقاتك من authentik، وMoodle من التطبيقات التي تحتاج مصدراً آخر للأدوار.

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

الرسالةالسببالحل
لدواعي الأمان، فقط الاتصالات عبر بروتوكول https مسموح بهاعنوان المزود يبدأ بـ httpعنوان HTTPS لـ authentik، ولا يوجد إعداد يتجاوز ذلك
Could not discover service endpoints: تم حظر عنوان URLعنوان authentik في نطاق خاص محظور، أو منفذ غير مسموحالوصول بالنطاق العام على 443، أو إزالة أصغر نطاق ممكن من curlsecurityblockedhosts
فشل الاكتشاف مع خطأ شهادة SSLMoodle لا يثق بالشهادةشهادة عامة صحيحة، أو إضافة جهة الإصدار الداخلية إلى مخزن الشهادات في سيرفر Moodle
قم بتأكيد اشتراكك، وبريد لا يصلالتأكيد بالبريد مفعل في الخدمةإلغاء Require Email Verification، ثم تأكيد الحسابات المعلقة من قائمة المستخدمين
تعذر العثور على حساب بعنوان بريدك الالكترونيمنع إنشاء الحسابات مفعل والحساب غير موجودإنشاء الحساب من نظام الطلاب بالبريد نفسه الموجود في authentik
الصلاحية مرفوضة في صفحة authentikالمستخدم ليس في مجموعة مربوطة بتطبيق Moodleإضافته إلى lms-students أو lms-teachers
اسم المستخدم في Moodle هو البريدلا يوجد ربط لـ preferred_usernameربط الحقل قبل أول دخول، وتصحيح الحسابات القديمة يدوياً

النسخ الاحتياطي والترقية

  • النسخ الاحتياطي Backup: إعدادات الخدمة وروابط الحقول وجدول الربط Linked Logins كلها في قاعدة بيانات Moodle، فتحميها النسخة الكاملة التي شرحناها في دليل Moodle، وأما المستخدمون والمجموعات والمزود والشهادة ففي قاعدة بيانات authentik، كما في دليل authentik. وخذ النسختين في الوقت نفسه، لأن الطالب الموجود في واحدة دون الأخرى لا يستطيع الدخول.
  • الترقية Upgrade: بعد ترقية authentik جرب الدخول بحساب طالب وحساب معلم قبل أن تفتح الموقع، وبعد ترقية Moodle راجع أن إعدادات curlsecurityblockedhosts وcurlsecurityallowedport ما زالت كما ضبطتها. والإصدار الذي جربناه authentik 2026.8.4 صدر في 14 أكتوبر 2026 بإصلاحات فقط.
  • حساب الطوارئ Break-glass: أبق حساب المدير المحلي في Moodle بكلمة مرور قوية ونموذج الدخول المحلي ظاهراً للمدراء، والسبب أن توقف authentik يعني أن لا أحد يدخل بالزر، بما فيهم من سيصلح المشكلة.

تنظيف المختبر

docker compose down -v

ثم احذف ال Docker Images التي حملتها للمختبر إذا لم تعد تحتاجها، بأسمائها الكاملة، واحذف مجلد tls لأن فيه مفتاح جهة الإصدار.

تدريب عملي

  • أنشئ تدفق دخول مستقلاً للطلاب في authentik بمدة جلسة 45 دقيقة، واربطه بتطبيق Moodle فقط، ثم جرب الخروج والدخول من جديد بعد انتهاء المدة.
  • أضف الطالب s2026002 إلى ملف CSV مع زمرة students-2026، وفعل طريقة التسجيل بمزامنة الزمرة في مقرر CS101، ثم ادخل بحسابه وتأكد أن المقرر ظهر له.

الخلاصة

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

  • Moodle يتصل بـ authentik عبر إضافة OAuth 2 الأساسية، ويطلب عنوان HTTPS وشهادة يثق بها سيرفر Moodle نفسه، وعنواناً لا يقع في نطاقات الشبكة الخاصة المحظورة.
  • الإعداد الافتراضي ينشئ حساباً معلقاً باسم مستخدم هو البريد، والإعداد الصحيح: إيقاف التأكيد بالبريد، وربط preferred_username، والأسماء العربية من authentik، وكل ذلك قبل أول طالب.
  • اجعل نظام شؤون الطلاب ينشئ الحسابات والتسجيل بملف CSV بعمود auth=oauth2، وفعل authpreventaccountcreation، فيربط Moodle الدخول بالحساب بالبريد ويرفض من ليس له حساب.
  • مجموعات authentik تقرر من يدخل Moodle، ولا تتحول إلى أدوار ولا إلى تسجيل في المقررات.
  • الخروج من Moodle لا ينهي جلسة authentik، ولأجهزة المعامل قصر مدة الجلسة واجعل المتصفح يحذف بياناته عند الإغلاق.

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

  • أكتوبر 2026: كتابة المقال وتجربته على Moodle 5.3.0 وauthentik 2026.8.4 وPostgreSQL 17.11.
Could not load content
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.