في أول أسبوع من الفصل الدراسي يأتي إلى الدعم الفني سؤال واحد يتكرر مئات المرات: «نسيت كلمة مرور المنصة»، والسبب أن الطالب يحمل كلمة مرور للبريد، وأخرى لنظام الطلاب، وثالثة لمنصة التعلم 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 أبداً: يكتبها الطالب في صفحة 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-configuration000 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=curlsecurityallowedport127.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 -> picture169.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 يحاول تخمين أيقونة للمزود المخصص ولم يجد شيئاً، فاكتب رابط شعار المدرسة في حقل Logo URL في إعدادات الخدمة. والفاصل بين النموذج والزر مكتوب عليه «تمام» في الترجمة العربية، ويمكن تصحيحه من أداة تخصيص اللغة Language Customisation. وعند الضغط على الزر ينتقل الطالب إلى صفحة authentik:

دخل الطالب s2026001 بكلمة مروره، فعاد إلى 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والشكل التالي يلخص تقسيم العمل الذي نريده:

الحسابات من نظام شؤون الطلاب عبر 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-2026docker 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 ربطاً سابقاً للمستخدم بحث عن حساب بالبريد نفسه، فإذا وجده وكان التأكيد بالبريد موقوفاً ربطه مباشرة، وإذا لم يجده ومنع إنشاء الحسابات مفعل رفض الدخول. والصورتان التاليتان للحالتين الثانية والثالثة:


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

تحديث الاسم في كل دخول
غيرنا الاسم الأخير للمعلمة في 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 |
| فشل الاكتشاف مع خطأ شهادة SSL | Moodle لا يثق بالشهادة | شهادة عامة صحيحة، أو إضافة جهة الإصدار الداخلية إلى مخزن الشهادات في سيرفر 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.