إضافة SSO إلى أي تطبيق باستخدام oauth2-proxy
ضع أي تطبيق بلا تسجيل دخول خلف مزود OIDC عبر oauth2-proxy وNginx أو Traefik، وتجنب خطأ 401 ومشكلات ملفات تعريف الارتباط وإعادة التوجيه.
المصادقة بالإنابة: كيف تحصل تطبيقات بلا تسجيل دخول على SSO
يوفّر oauth2-proxy ميزة تسجيل الدخول الموحّد لتطبيق لا يملك نظام تسجيل دخول خاصاً به. يعمل ذلك لأن الـreverse proxy أمام التطبيق يوقف كل طلب، ويسأل oauth2-proxy عما إذا كان الطلب يتضمن جلسة صالحة، ولا يمرر الطلب إلى الخدمة الخلفية إلا عندما تكون الإجابة نعم. لا يتغير رمز التطبيق، لأن التطبيق لا يرى عملية التحقق.
تتكون عملية التحقق من طلب HTTP إضافي واحد. يرسل الـproxy نسخة من رؤوس الطلب الوارد إلى /oauth2/auth ويقرأ رمز الحالة. تعني القيمة 202 أن لدى العميل جلسة، لذلك يمرر الـproxy الطلب الأصلي إلى التطبيق. وتعني القيمة 401 عدم وجود جلسة، لذلك يوجّه الـproxy المتصفح إلى /oauth2/sign_in، الذي يبدأ تسجيل دخول OpenID Connect (OIDC) لدى موفر الهوية لديك. OIDC هي طبقة الهوية المبنية فوق OAuth 2.0، وموفر الهوية هو النظام الذي تستخدمه بالفعل لتسجيل الدخول.
يحمل هذا النمط اسماً في كل reverse proxy. يسمّي Nginx التوجيه auth_request. ويسمّي Traefik الـmiddleware forwardAuth. ويكتب Caddy اسمه forward_auth. كما أن الخدمة التي تجيب عن الطلب الفرعي قابلة للاستبدال. ويُعد oauth2-proxy الخيار الشائع لأنه يتحدث OIDC مباشرة ولا يحتاج إلى قاعدة بيانات خاصة به.
حدّد حدود الثقة قبل كتابة أي إعدادات
بعد نجاح التحقق، يعيد oauth2-proxy الهوية في ترويسات الاستجابة، وينسخها الـreverse proxy إلى طلب الخدمة الخلفية. عند تفعيل set_xauthrequest تحصل على X-Auth-Request-User وX-Auth-Request-Email. يقرأ التطبيق هذه الترويسات ويثق بها.
هذا هو نموذج الأمان بالكامل، لذا اذكر النتيجة بوضوح. يستطيع أي طرف يمكنه فتح اتصال TCP بمنفذ التطبيق ضبط هذه الترويسات بنفسه وانتحال هوية أي مستخدم. ويؤدي وجود curl -H "X-Auth-Request-Email: admin@example.com" http://app-host:3000/ واحد يصل مباشرةً إلى التطبيق إلى تجاوز كامل للحماية.
لذلك يجب ألا يكون التطبيق قابلاً للوصول إلا عبر الـproxy. في Docker Compose، احذف تعيين ports: من خدمة التطبيق واتركها على الشبكة الداخلية، بحيث لا يتمكن من الاتصال بها إلا حاوية الـproxy. على خادم مستقل، اربط التطبيق بالعنوان 127.0.0.1:3000 بدلاً من 0.0.0.0:3000. ثم تحقّق مما عرّضته فعلياً:
sudo ss -tlnp | grep 3000يعني السطر الذي يقرأ 0.0.0.0:3000 أن التطبيق يستجيب على عنوان IP العام، وأن بوابة الحماية شكلية. أما 127.0.0.1:3000 فهو ما تريده. وتُعد قاعدة جدار ناري طبقة ثانية مفيدة، لكن عنوان الربط هو الإعداد الذي يبقى فعالاً حتى بعد أن تقوم أداة أخرى بمسح مجموعة القواعد لديك.
ثبّت oauth2-proxy
اعتباراً من August 2026، الإصدار الحالي هو v7.15.3، وقد صدر في June 2026. ثبّت الملف التنفيذي وتحقق من التنزيل:
cd /tmp
curl -fsSLO https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.15.3/oauth2-proxy-v7.15.3.linux-amd64.tar.gz
curl -fsSLO https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.15.3/oauth2-proxy-v7.15.3.linux-amd64.tar.gz-sha256sum.txt
sha256sum -c oauth2-proxy-v7.15.3.linux-amd64.tar.gz-sha256sum.txt
tar -xzf oauth2-proxy-v7.15.3.linux-amd64.tar.gz
sudo install -m 755 oauth2-proxy-v7.15.3.linux-amd64/oauth2-proxy /usr/local/bin/oauth2-proxy
oauth2-proxy --versionيجب أن يطبع sha256sum -c سطراً ينتهي بـ OK. إذا طبع FAILED، فتوقف وأعد التنزيل بدلاً من تشغيل الملف التنفيذي.
في Docker، تكون الصورة quay.io/oauth2-proxy/oauth2-proxy، وعليك تثبيت الوسم: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3. يؤدي تركه على latest إلى تحويل docker compose pull روتينياً إلى ترقية غير مخطط لها للعملية التي تحمي كل تطبيق على الخادم.
إنشاء سر ملف تعريف الارتباط
ملف تعريف ارتباط الجلسة مشفّر، وcookie_secret هو المفتاح. يجب أن يكون طول المفتاح 16 أو 24 أو 32 بايتاً بالضبط، لأنه يصبح مفتاح AES (معيار التشفير المتقدم). عند استخدام أي طول آخر، يرفض oauth2-proxy بدء التشغيل ويعرض خطأً عند بدء التشغيل يذكر سر ملف تعريف الارتباط.
openssl rand -base64 32 | tr -- '+/' '-_'إنّ tr ليس إجراءً شكلياً. فهو يحوّل base64 القياسي إلى أبجدية آمنة للاستخدام في عناوين URL، ولذلك تبقى القيمة صالحة عند تمريرها عبر shell وملف env ورأس HTTP من دون مشكلات في علامات الاقتباس.
اتبع قاعدتين لهذه القيمة. استخدم سراً مختلفاً لكل عملية نشر. وعند تشغيل أكثر من مثيل واحد من oauth2-proxy خلف النطاق نفسه، استخدم السر نفسه في جميع المثيلات، لأن ملف تعريف الارتباط الذي يشفّره أحد المثيلات يجب أن تتمكن المثيلات الأخرى من قراءته.
اكتب إعدادات oauth2-proxy
احتفظ بالإعدادات في ملف بدلاً من استخدام سطر أوامر طويل، حتى لا يظهر سر العميل في مخرجات ps.
# /etc/oauth2-proxy/oauth2-proxy.cfg
http_address = "127.0.0.1:4180"
reverse_proxy = true
provider = "oidc"
oidc_issuer_url = "https://id.example.com/application/o/myapp/"
client_id = "REPLACE_ME"
client_secret = "REPLACE_ME"
redirect_url = "https://app.example.com/oauth2/callback"
cookie_secret = "REPLACE_ME"
cookie_secure = true
cookie_domains = [".example.com"]
whitelist_domains = [".example.com"]
email_domains = ["*"]
set_xauthrequest = true
upstreams = ["static://202"]يخبر reverse_proxy = true oauth2-proxy بأن يثق في رؤوس X-Forwarded-* التي يرسلها الـproxy الموجود أمامه. من دونه، يتعامل oauth2-proxy مع عنوان الـproxy نفسه على أنه عنوان العميل، وقد يحدد بشكل خاطئ ما إذا كان الطلب قد وصل عبر HTTPS.
يجعل upstreams = ["static://202"] oauth2-proxy يعيد الاستجابة 202 للطلب الذي تمت مصادقته من دون تمرير أي شيء، وهذا هو المطلوب تماماً في forward auth، لأن الـreverse proxy هو الذي يتولى تمرير الطلب. أما نمط النشر الآخر، فيضع oauth2-proxy مباشرة في مسار الطلب باستخدام upstreams = ["http://127.0.0.1:3000"] ومن دون auth_request إطلاقاً. هذا أبسط لتطبيق واحد، لكنه لا يتوسع إلى عشرة تطبيقات.
يسمح email_domains = ["*"] بكل عنوان يمكن لموفر المصادقة الخاص بك مصادقته. قيّده بنطاقك الخاص، أو الأفضل من ذلك، قيّد الوصول عبر ربط مجموعة في موفر المصادقة، لأنك تدير المستخدمين هناك بالفعل.
شغّله عبر systemd باستخدام مستخدم مستقل:
# /etc/systemd/system/oauth2-proxy.service
[Unit]
Description=oauth2-proxy
After=network-online.target
Wants=network-online.target
[Service]
User=oauth2-proxy
Group=oauth2-proxy
ExecStart=/usr/local/bin/oauth2-proxy --config=/etc/oauth2-proxy/oauth2-proxy.cfg
Restart=on-failure
ProtectSystem=strict
PrivateTmp=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetsudo useradd --system --no-create-home --shell /usr/sbin/nologin oauth2-proxy
sudo install -d -m 750 /etc/oauth2-proxy
sudo chown -R oauth2-proxy:oauth2-proxy /etc/oauth2-proxy
sudo chmod 600 /etc/oauth2-proxy/oauth2-proxy.cfg
sudo systemctl daemon-reload
sudo systemctl enable --now oauth2-proxy
curl -s http://127.0.0.1:4180/pingتعني طباعة OK بواسطة /ping أن العملية بدأت وحمّلت إعداداتها. هذه هي نقطة فحص الصحة الخاصة بـoauth2-proxy، ولا تطلب جلسة أبداً. إذا لم يُجب أي شيء، فاقرأ journalctl -u oauth2-proxy -n 50، لأن عنوان issuer غير الصحيح وسر cookie ذي الطول غير الصحيح يفشلان عند بدء التشغيل، ويذكر السجل سبب الفشل في الحالتين.
سجّل URI إعادة التوجيه لدى موفّر الخدمة
أنشئ تطبيق OIDC لدى موفّر الخدمة، واضبط URI إعادة التوجيه ليطابق تماماً redirect_url الوارد في الإعدادات: https://app.example.com/oauth2/callback. تعني المطابقة التامة أن المخطط والمضيف والمنفذ والمسار يجب أن تتطابق حرفياً. وتؤدي الشرطة المائلة في النهاية إلى URI مختلف.
هذا هو سبب الفشل الأكثر شيوعاً في الإعداد بأكمله، ويحدث قبل مشاركة oauth2-proxy فيه. يرفض موفّر الخدمة طلب التفويض ويعرض صفحة الخطأ الخاصة به، لذلك لا يظهر أي شيء في سجل oauth2-proxy. وتتمثل العلامة الواضحة في شريط العنوان: يبقى المتصفح على نطاق موفّر الخدمة، وتحمل سلسلة الاستعلام error=invalid_request أو تسمّي الصفحة redirect_uri مباشرة. عند رؤية ذلك، أصلح سجل التطبيق لدى موفّر الخدمة، لا إعدادات الوكيل.
انسخ URL الخاص بالجهة المصدرة من موفّر الخدمة بدلاً من كتابته يدوياً. يضيف oauth2-proxy القيمة /.well-known/openid-configuration إلى oidc_issuer_url، ويجلب مستند الاكتشاف هذا عند بدء التشغيل. تحقّق منه بنفسك أولاً:
curl -s https://id.example.com/application/o/myapp/.well-known/openid-configuration | head -c 400يعني JSON الذي يحتوي على مفتاح authorization_endpoint أن URL الخاص بالجهة المصدرة صحيح. أما ظهور 404 أو صفحة خطأ بتنسيق HTML فيعني أن URL غير صحيح، وسيفشل oauth2-proxy في بدء التشغيل بسبب 404 نفسه. إذا لم تختر موفّر خدمة بعد، فراجع مقارنة Keycloak وAuthentik وZitadel لمعرفة المفاضلات، واتبع تشغيل Authentik كخادم SSO خاص بك لإعداد جانب موفّر الخدمة في هذا الإعداد بالتحديد.
Nginx: auth_request
ينفّذ Nginx المصادقة بالوكالة باستخدام auth_request، الذي يرسل طلباً فرعياً داخلياً ويتخذ الإجراء وفق رمز حالته.
# in the http context, next to your other maps
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
server_name app.example.com;
location /oauth2/ {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Auth-Request-Redirect $request_uri;
}
location = /oauth2/auth {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}
location / {
auth_request /oauth2/auth;
error_page 401 = @oauth2_signin;
auth_request_set $user $upstream_http_x_auth_request_user;
auth_request_set $email $upstream_http_x_auth_request_email;
proxy_set_header X-User $user;
proxy_set_header X-Email $email;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location @oauth2_signin {
return 302 /oauth2/sign_in?rd=$scheme://$host$request_uri;
}
}توجد هنا ثلاثة تفاصيل مهمة. يؤدي proxy_pass_request_body off مع Content-Length فارغ إلى منع nginx من نسخ جسم كل طلب POST إلى الطلب الفرعي، وهذا مهم لأن oauth2-proxy لا يقرأه. عند رفع ملف، يرسل الإعداد الافتراضي الملف مرتين.
يمرّر الزوج auth_request_set $auth_cookie وadd_header Set-Cookie ملف تعريف ارتباط الجلسة المحدَّث إلى المتصفح. إذا حذفته، فلن يفعل cookie_refresh شيئاً بصمت، لأن nginx يتجاهل Set-Cookie الخاص بالطلب الفرعي، ويحتفظ المتصفح بالقيمة القديمة حتى تنتهي الجلسة.
يحوّل error_page 401 = @oauth2_signin فشل التحقق إلى تسجيل دخول. من دونه، يحصل الزائر غير المصادق عليه على صفحة 401 Authorization Required مجردة ولا يجد طريقة للمتابعة.
اختبر قبل إعادة التحميل دائماً:
sudo nginx -t && sudo systemctl reload nginxإذا كانت التوجيهات المحيطة جديدة عليك، يشرح تشريح إعداد reverse proxy في nginx الطبقة الأساسية التي يعتمد عليها هذا الإعداد.
Traefik: البرمجية الوسيطة forwardAuth
يحتاج Traefik إلى برمجيتين وسيطتين للمهمة نفسها. تنفّذ إحداهما الفحص. وتحول الأخرى الاستجابة 401 إلى إعادة توجيه في المتصفح.
# dynamic configuration
http:
middlewares:
oauth-auth:
forwardAuth:
address: https://oauth.example.com/oauth2/auth
trustForwardHeader: true
oauth-errors:
errors:
status:
- "401-403"
service: oauth-backend
query: "/oauth2/sign_in?rd={url}"
statusRewrites:
"401": 302اربط البرمجيتين الوسيطتين كلتيهما بالموجّه الذي يواجه تطبيقك، وانشر oauth2-proxy على موجّه مستقل عند oauth.example.com، لأن المتصفح يجب أن يصل إلى /oauth2/sign_in و/oauth2/callback من دون المرور عبر الفحص.
statusRewrites الذي يحوّل 401 إلى 302 هو الجزء الذي يغفل عنه المستخدمون غالباً. من دونه يعيد Traefik إعادة توجيه تسجيل الدخول مع حالة 401، ولا يتبعها المتصفح، وتظهر للزائر صفحة تحتوي على الكلمة الوحيدة Found.
يمرّر trustForwardHeader: true اسم المضيف وURI الأصليين إلى oauth2-proxy، الذي يحتاج إليهما لإنشاء قيمة rd التي تعيد المستخدم إلى الصفحة التي طلبها. اضبط whitelist_domains بحيث يشمل ذلك المضيف أيضاً، وإلا سيسقط oauth2-proxy المعامل rd باعتباره خطراً لإعادة التوجيه المفتوح، وينتهي الجميع إلى / بعد تسجيل الدخول. يواجه خادم Traefik عادة عدة تطبيقات في الوقت نفسه، ويوضح توجيه عدة تطبيقات Docker Compose عبر مثيل Traefik واحد تخطيط الموجّه الذي يندمج معه هذا الإعداد.
Caddy: forward_auth
app.example.com {
handle /oauth2/* {
reverse_proxy oauth2-proxy.internal:4180 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Uri {uri}
}
}
handle {
forward_auth oauth2-proxy.internal:4180 {
uri /oauth2/auth
header_up X-Real-IP {remote_host}
copy_headers X-Auth-Request-User X-Auth-Request-Email
@error status 401
handle_response @error {
redir * /oauth2/sign_in?rd={scheme}://{host}{uri}
}
}
reverse_proxy upstream.internal:3000
}
}الترتيب مهم هنا. تأتي كتلة /oauth2/* أولاً ولا تحتوي على forward_auth، لأن الزائر غير المسجّل عليه الوصول إلى مسارات تسجيل الدخول وcallback. إذا وضعت الفحص قبل هذه المسارات، فسيعيد تسجيل الدخول التوجيه إلى نفسه حتى يتوقف المتصفح عن المحاولة.
تعمل copy_headers على نقل الهوية إلى الطلب الموجّه إلى الخدمة الخلفية، ولا تنتج قيماً إلا عند تشغيل oauth2-proxy مع set_xauthrequest = true. اختيار proxy المناسب موضوع مستقل، وتتناوله المقارنة بين nginx وCaddy وTraefik.
لماذا تعود عملية تسجيل الدخول إلى صفحة تسجيل الدخول؟
تسجّل الدخول لدى موفّر الهوية، ثم يعيدك إليه، وبعد ذلك يرسل oauth2-proxy طلبك مباشرةً إلى الموفّر مرة أخرى. تعني الحلقة أن طلب callback وصل من دون ملف تعريف الارتباط الذي عيّنه oauth2-proxy أثناء إعادة التوجيه. يذكر السجل هذه الحالة:
No cookies were found in OAuth callback.أو عندما يصل ملف تعريف ارتباط آخر، لكن ملف التعريف الصحيح لا يصل:
Cookies were found in OAuth callback, but none was a CSRF cookie.يشير CSRF إلى تزوير الطلبات عبر المواقع، ويُستخدم ملف تعريف الارتباط هذا لربط callback بعملية تسجيل الدخول التي بدأت الطلب. يعرض المتصفح الفشل نفسه على النحو التالي:
Login Failed: Unable to find a valid CSRF token. Please try again.تحقق من هذه الأسباب الأربعة بالترتيب.
cookie_secure = trueبينما وصل المتصفح إلى الموقع عبر HTTP عادي. لن يخزّن المتصفح ملف تعريف ارتباط يحمل السمةSecureعلى أصلhttp://، ولذلك لن يعيده في الطلب. أنهِ TLS (أمان طبقة النقل) بطريقة صحيحة، أو اضبطcookie_secure = falseفقط أثناء الاختبار على localhost.- قيمة
cookie_domainsلا تغطي اسم المضيف الظاهر في شريط العناوين. تغطي.example.comاسم المضيفapp.example.com، ولا تؤثر إطلاقاً فيapp.example.net. - المتصفح يحذف ملف تعريف الارتباط. قد تزيل إضافة خصوصية صارمة أو ميزة حظر ملفات تعريف ارتباط الجهات الخارجية
_oauth2_proxy_csrfبين إعادة التوجيه الصادرة وcallback. - انحراف الساعة. إذا كانت ساعة الخادم تختلف كثيراً عن ساعة الموفّر، فستقع قيمتا
iatوexpفي رمز ID token خارج النافذة المقبولة، وتُرفض الجلسة عند وصولها. يجب أن يعرضtimedatectlالقيمةSystem clock synchronized: yes.
راقب ما يحدث من جهة الخادم بدلاً من التخمين:
sudo journalctl -u oauth2-proxy -fافتح التطبيق في نافذة خاصة. يُسجَّل كل طلب مع حالته، ولذلك فإن callback يتبعه مباشرةً إعادة توجيه أخرى إلى الموفّر هو الحلقة المسجّلة فعلياً.
المسارات التي يجب أن تتجاوز تسجيل الدخول: واجهات API وخطافات الويب وWebSockets
تفترض المصادقة المُمرَّرة وجود متصفح يحتفظ بملف تعريف ارتباط. لذلك تفشل الطلبات الواردة من دون متصفح.
لا يملك عميل API الذي يرسل Authorization: Bearer <token> ملف تعريف ارتباط، لذلك يتلقى استجابة 302 إلى صفحة تسجيل الدخول لدى موفّر الخدمة، ثم يحاول تحليل HTML على أنه JSON. هناك حلّان واضحان. يتيح ضبط skip_jwt_bearer_tokens = true لـoauth2-proxy قبول رمز Bearer مميز صالح بتنسيق JWT (JSON web token) من المُصدِر نفسه. وهذا مناسب عندما تحصل عملاء API لديك على الرموز المميزة من موفّر الخدمة. وإلا فاستثنِ المسار:
skip_auth_routes = [
"^/api/",
"POST=^/webhook/",
"GET=^/healthz$"
]كل قيمة هي تعبير عادي يُطابَق مع المسار المطبَّع، ويمكن أن يسبقها اختيارياً أسلوب HTTP و=. يترك POST=^/webhook/ مستقبِل خطاف الويب مفتوحاً أمام POST، بينما يظل المستخدم الذي يتصفح المسار نفسه مطالباً بتسجيل الدخول. كل إدخال هو ثغرة في بوابة الحماية، لذلك ثبّت التعبيرات باستخدام ^ واجعل نطاقها أضيق ما يسمح به العميل.
تُعد WebSockets الحالة التي يخطئ الناس في التعامل معها. طلب الترقية هو طلب HTTP GET عادي يحمل ملفات تعريف الارتباط نفسها التي يحملها أي طلب آخر، لذلك يمر بالفحص بشكل طبيعي ولا يحتاج إلى استثناء. ما يتعطل هو إعداد الوكيل حوله. من دون رأسي Upgrade وConnection في الموقع المحمي، لا تكتمل الترقية، ويعيد عميل التطبيق المحاولة إلى ما لا نهاية مع ظهور رسالة WebSocket connection ... failed في وحدة تحكم المتصفح. لا يصلح استثناء المسار هذه المشكلة، لأن الطلب كان قد خضع للمصادقة مسبقاً.
يوجد حد فعلي واحد. يُجرى الفحص مرة واحدة عند الترقية. ولا يُعاد فحص اتصال WebSocket الذي يبقى مفتوحاً لساعات، لذلك لا يؤدي حذف مستخدم من موفّر الخدمة إلى إغلاق المقبس الذي يحتفظ به بالفعل. أعد تشغيل التطبيق لقطع الاتصالات النشطة.
تخزين الجلسة، وملف تعريف الارتباط الذي يكبر أكثر من اللازم
بشكل افتراضي، تُخزَّن الجلسة كاملة داخل ملف تعريف الارتباط، وتُشفَّر باستخدام cookie_secret. يحافظ ذلك على عمل oauth2-proxy دون حالة، ولا يتطلب خدمة إضافية. وله أيضاً حد أقصى، لأن المتصفحات تضع حداً لحجم ملف تعريف الارتباط يقارب 4 KB. عندما يحمل رمز ID قائمة طويلة من مطالبات المجموعات، يقسم oauth2-proxy الجلسة بين _oauth2_proxy_0 و_oauth2_proxy_1 وما بعدهما. وبعد بضعة أجزاء، تكبر رؤوس الطلبات بما يكفي لكي يعيد nginx الاستجابة 400 Request Header Or Cookie Too Large قبل أن يرى التطبيق الطلب.
انقل الجلسة إلى جانب الخادم عند حدوث ذلك:
session_store_type = "redis"
redis_connection_url = "redis://127.0.0.1:6379"يحتفظ المتصفح عندئذ بتذكرة قصيرة، بينما تُخزَّن الجلسة المشفَّرة في Redis. وتتمثل الكلفة في ضرورة استمرار خدمة Redis في العمل: فإذا توقفت Redis، تصبح كل جلسة غير صالحة، ويتم تسجيل خروج الجميع في الوقت نفسه. أما مخزن ملفات تعريف الارتباط فلديه كلفة خاصة به؛ إذ يمكن لطلبين يحدّثان الجلسة نفسها في اللحظة ذاتها أن يتعارضا، ما يفرض تسجيل الدخول مرة أخرى.
ما الذي لا تمنحك إياه مصادقة forward auth
هذه بوابة عند نقطة الدخول. وليست تفويضاً داخل التطبيق، وهذا الفرق يحدد مدى ملاءمة هذا الأسلوب لحالتك.
بعد عبور المستخدم، يرى التطبيق كل ما كان يراه من قبل. إذا كان للتطبيق أدواره الخاصة، فلن تملأ forward auth هذه الأدوار، إلا إذا كان التطبيق يدعم المصادقة المستندة إلى الرؤوس ويربط أحد الرؤوس بحساب. يدعم Grafana ذلك من خلال إعدادات auth.proxy. لكن معظم التطبيقات ذاتية الاستضافة لا تدعم ذلك، لذلك يكون كل من يعبر البوابة هوية واحدة نفسها بالنسبة إلى التطبيق، وغالباً ما تكون هذه الهوية حساب admin.
كما أنها لا تحمي رموز API الخاصة بالتطبيق. يصادق personal access token الذي يصدره التطبيق على التطبيق نفسه، وليس على oauth2-proxy، لذلك يتوقف الرمز عن العمل فور وضع البوابة أمامه. إذا استثنيت مسار API لإعادته إلى العمل، يصبح ذلك الرمز هو الشيء الوحيد الذي يحمي المسار. عندها تشغّل نظامي مصادقة على خدمة واحدة، ولا يغطي SSO إلا أحدهما.
إلغاء الوصول هو الفجوة الثالثة. يؤدي حذف مستخدم من موفر الهوية إلى إيقاف عمليات تسجيل الدخول الجديدة وإيقاف تحديث الرمز الذي ينفذه cookie_refresh، لكن تظل session cookie الحالية صالحة حتى تنتهي صلاحيتها. تكون قيمة cookie_expire الافتراضية 168 ساعة، أي أسبوع كامل من الوصول لشخص أزلته للتو. اضبط cookie_refresh على مدة قصيرة، مثل ساعة، حتى يحدث إلغاء الوصول ضمن هذه النافذة الزمنية.
يتوقف مسار التدقيق عند البوابة أيضاً. يسجّل oauth2-proxy من عبر البوابة ومتى عبرها. أما التطبيق، فيسجّل session غير مسماة. إذا احتجت إلى معرفة من غيّر إعداداً، فتمثل هوية المستخدم في رؤوس الطلب داخل التطبيق الحد الأدنى المطلوب، بينما تكون الحسابات الفعلية المنفصلة لكل مستخدم هي الحل الصحيح.
عندما يكون دفع تكلفة SSO هو الخيار الأفضل
تُعد المصادقة الأمامية الأداة المناسبة عندما لا يوفّر التطبيق تسجيل دخول مطلقاً، أو يستخدم كلمة مرور مشتركة واحدة، وتريد مكاناً واحداً لإضافة المستخدمين وإزالتهم. تتطلب هذه الطريقة فترة عمل بعد الظهر وprocess إضافية واحدة، وتعمل مع أي تطبيق يستخدم HTTP.
وتكون الأداة غير مناسبة عندما يحتاج المستخدمون المختلفون إلى صلاحيات مختلفة داخل التطبيق نفسه. فلا يمكن لبوابة الوصول التعبير عن أن «Ana يمكنها تعديل لوحات المعلومات، بينما لا يستطيع Bo سوى قراءتها». إذا كان المورّد يبيع فئة SSO، فعادةً ما يكون ربط المجموعات بالأدوار هو الميزة التي تشتريها فعلياً، وإعادة بناء ذلك باستخدام الرؤوس وقواعد الـproxy أكثر عرضة للأخطاء من دفع تكلفتها. تستحق نمط التسعير الكامن وراء فئات SSO هذه القراءة قبل أن تحسم قرارك.
وتشير حالتان أخريان إلى الاتجاه نفسه. لن تقبل أعمال الامتثال التي تحتاج إلى سجلات تدقيق لكل مستخدم داخل التطبيق سجل وصول الـproxy دليلاً. كما أن أي تطبيق يملك عميلاً للهواتف المحمولة أو سطح المكتب ولا يرسل cookies الخاصة بالمتصفح سيواجه البوابة في كل طلب.
FAQ
ما المقصود بالمصادقة التمريرية؟
المصادقة التمريرية هي نمط يطلب فيه الـReverse Proxy من خدمة مصادقة منفصلة التحقق من كل طلب وارد قبل تمريره إلى الخدمة الخلفية. يرسل الـProxy رؤوس الطلب إلى نقطة نهاية مثل /oauth2/auth ويقرأ رمز الحالة. تعني 202 أن الطلب مسموح به، ولذلك يمرر الطلب الأصلي إلى التطبيق. وتعني 401 عدم وجود جلسة، ولذلك يعيد الـProxy توجيه المتصفح إلى صفحة تسجيل الدخول. يطبّق Nginx ذلك باستخدام التوجيه auth_request، وTraefik باستخدام middleware forwardAuth، وCaddy باستخدام forward_auth.
لماذا يعيد oauth2-proxy توجيهي إلى صفحة تسجيل الدخول في حلقة متكررة؟
وصل طلب callback إلى oauth2-proxy من دون cookie الخاصة بـCSRF، ولذلك يبدأ oauth2-proxy التدفق من جديد. يعرض سجل الخادم No cookies were found in OAuth callback.، ويعرض المتصفح Login Failed: Unable to find a valid CSRF token. Please try again.. السبب المعتاد هو cookie_secure = true على موقع وصل إليه المتصفح عبر HTTP عادي، لأن المتصفح لا يخزّن cookie من نوع Secure على origin من النوع http://. والسبب الشائع التالي هو قيمة cookie_domains التي لا تشمل اسم المضيف الظاهر في شريط العناوين.
كيف أسمح لعميل API أو webhook بالمرور عبر oauth2-proxy؟
استخدم skip_auth_routes مع regular expression مثبتة الحدود، ويمكنك اختيارياً تقييدها بطريقة HTTP واحدة، مثل POST=^/webhook/. إذا كانت عملاء API لديك تملك مسبقاً JWTs صادرة عن المزوّد نفسه، فإن skip_jwt_bearer_tokens = true تقبل تلك الرموز بدلاً من cookie وتحافظ على حماية المسار. كل ما تُدرجه في skip_auth_routes يصبح غير خاضع للمصادقة للجميع، لذلك اجعل كل تعبير أضيق نطاق يسمح به المستدعي.
هل يمنح oauth2-proxy التطبيق صلاحيات لكل مستخدم؟
لا. إنّه بوابة، وليس نظاماً للتفويض. يقرر من يمكنه الوصول إلى التطبيق، ويظهر كل من يصل إليه بالطريقة نفسها للتطبيق، ما لم يقرأ التطبيق رؤوس الهوية ويربطها بالحسابات. يمكن لـGrafana تنفيذ ذلك من خلال إعدادات auth.proxy. أما معظم التطبيقات ذاتية الاستضافة فلا تستطيع ذلك، ولذلك يشترك كل شخص يتجاوز البوابة في الهوية الوحيدة التي يعمل التطبيق بها.
هل يمكن لأحد تجاوز oauth2-proxy بتعيين رأس الهوية بنفسه؟
نعم، إذا كان يستطيع الوصول إلى التطبيق مباشرة. تصل الهوية في رأس عادي مثل X-Auth-Request-Email، ويثق التطبيق بكل ما يتلقاه. يستطيع أي شخص يمكنه فتح اتصال بمنفذ التطبيق إرسال ذلك الرأس وانتحال هوية أي مستخدم. اربط التطبيق بالعنوان 127.0.0.1، أو أبقه على شبكة Docker داخلية من دون منفذ منشور، وتحقق من ذلك باستخدام sudo ss -tlnp.