SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-21

پیاده‌سازی SSO برای برنامه‌ها با oauth2-proxy

با استفاده از قابلیت Forward Auth در oauth2-proxy، بدون تغییر در کد برنامه، احراز هویت OIDC را به Nginx یا Traefik اضافه کنید. از خطاهای رایج کوکی و ریدایرکت جلوگیری کنید.

احراز هویت Forward: چگونه یک برنامه بدون سیستم ورود، SSO دریافت می‌کند

ابزار oauth2-proxy قابلیت single sign-on را برای برنامه‌ای که سیستم ورود اختصاصی ندارد، فراهم می‌کند. این کار به این صورت انجام می‌شود که reverse proxy که در مقابل آن برنامه قرار دارد، هر درخواست را متوقف می‌کند، از oauth2-proxy می‌پرسد که آیا درخواست دارای session معتبر است یا خیر، و تنها زمانی درخواست را به سمت upstream هدایت می‌کند که پاسخ مثبت باشد. کد برنامه هرگز تغییر نمی‌کند، زیرا برنامه اصلاً متوجه این بررسی نمی‌شود.

این بررسی شامل یک درخواست HTTP اضافی است. پروکسی یک کپی از هدرهای درخواست ورودی را به /oauth2/auth ارسال کرده و کد وضعیت را می‌خواند. کد 202 به این معنی است که تماس‌گیرنده دارای session است، بنابراین پروکسی درخواست اصلی را به برنامه هدایت می‌کند. کد 401 به معنی نبود session است، بنابراین پروکسی مرورگر را به /oauth2/sign_in می‌فرستد که فرآیند ورود OpenID Connect (OIDC) را در identity provider شما آغاز می‌کند. OIDC لایهٔ هویتی است که بر پایه OAuth 2.0 ساخته شده و provider همان سرویسی است که هم‌اکنون برای ورودها از آن استفاده می‌کنید.

این الگو در هر reverse proxy نام خاصی دارد. Nginx از دستور auth_request استفاده می‌کند. Traefik آن را middleware با نام forwardAuth می‌نامد. Caddy آن را forward_auth می‌خواند. سرویسی که به این زیر-درخواست (sub-request) پاسخ می‌دهد نیز قابل جایگزینی است. oauth2-proxy انتخاب رایجی است زیرا از OIDC استاندارد پشتیبانی می‌کند و نیازی به دیتابیس اختصاصی ندارد.

پیش از نوشتن هرگونه پیکربندی، مرز اعتماد را ترسیم کنید

پس از بررسی موفقیت‌آمیز، oauth2-proxy هویت کاربر را در قالب هدرهای پاسخ بازمی‌گرداند و reverse proxy آن‌ها را به درخواست upstream کپی می‌کند. با فعال‌سازی set_xauthrequest، شما X-Auth-Request-User و X-Auth-Request-Email را دریافت خواهید کرد. برنامه این هدرها را می‌خواند و به آن‌ها اعتماد می‌کند.

این کل مدل امنیتی است، بنابراین پیامد آن را صریح بیان کنید. هر چیزی که بتواند یک اتصال TCP به پورت برنامه برقرار کند، می‌تواند خودش این هدرها را تنظیم کرده و به هر کاربری تبدیل شود. یک curl -H "X-Auth-Request-Email: admin@example.com" http://app-host:3000/ واحد، در صورت دسترسی مستقیم به برنامه، یک دور زدن کامل امنیتی محسوب می‌شود.

بنابراین، برنامه نباید به جز از طریق پروکسی قابل دسترسی باشد. در Docker Compose، نگاشت ports: را از سرویس برنامه حذف کنید و آن را در شبکه داخلی باقی بگذارید تا فقط کانتینر پروکسی بتواند به آن متصل شود. روی یک میزبان خام (bare host)، برنامه را به جای 0.0.0.0:3000، روی 127.0.0.1:3000 bind کنید. سپس بررسی کنید که واقعاً چه چیزی را در معرض دید قرار داده‌اید:

sudo ss -tlnp | grep 3000

خطی که 0.0.0.0:3000 را نشان می‌دهد به این معنی است که برنامه روی IP عمومی پاسخ می‌دهد و دروازه شما صرفاً جنبه تزئینی دارد. 127.0.0.1:3000 همان چیزی است که به آن نیاز دارید. یک قانون فایروال لایه دوم مفیدی است، اما آدرس bind چیزی است که حتی پس از پاکسازی مجموعه قوانین توسط ابزارهای دیگر نیز باقی می‌ماند.

نصب oauth2-proxy

تا اوت 2026، نسخه فعلی v7.15.3 است که در ژوئن 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 معمولی به ارتقای ناخواسته تنها فرآیندی تبدیل شود که از تمام برنامه‌های موجود روی سرور محافظت می‌کند.

کوکی نشست (session cookie) رمزنگاری‌شده است و cookie_secret کلید آن محسوب می‌شود. طول این کلید باید دقیقاً 16، 24 یا 32 بایت باشد، زیرا به عنوان کلید AES (استاندارد رمزنگاری پیشرفته) استفاده می‌شود. در صورت استفاده از هر طول دیگری، oauth2-proxy اجرا نمی‌شود و خطای راه‌اندازی مربوط به cookie secret را نمایش می‌دهد.

openssl rand -base64 32 | tr -- '+/' '-_'

استفاده از tr صرفاً جنبه ظاهری ندارد. این دستور، base64 استاندارد را به الفبای ایمن برای URL تبدیل می‌کند تا مقدار نهایی بدون بروز مشکلات مربوط به کوتیشن، در shell، فایل‌های env و هدرهای HTTP به درستی منتقل شود.

برای این مقدار دو قانون وجود دارد. برای هر deployment از یک secret متفاوت استفاده کنید. همچنین هنگامی که بیش از یک نمونه oauth2-proxy را پشت یک دامنه واحد اجرا می‌کنید، برای همه آن‌ها از یک secret یکسان استفاده کنید؛ زیرا کوکی رمزنگاری‌شده توسط یک نمونه، باید توسط سایر نمونه‌ها نیز قابل خواندن باشد.

نوشتن فایل پیکربندی oauth2-proxy

تنظیمات را در یک فایل نگهداری کنید تا به جای استفاده از یک خط فرمان طولانی، client secret هرگز در خروجی 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-* ارسال‌شده از پروکسیِ جلویی اعتماد کند. بدون این گزینه، oauth2-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 = ["*"] هر آدرسی را که ارائه‌دهنده (provider) شما احراز هویت کند، می‌پذیرد. آن را به دامنه خود محدود کنید، یا بهتر است دسترسی را با استفاده از group binding در سمت ارائه‌دهنده محدود کنید، زیرا ارائه‌دهنده همان جایی است که شما از قبل کاربران را مدیریت می‌کنید.

آن را تحت 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.target
sudo 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

چاپ شدن /ping در OK به این معنی است که پردازش شروع شده و پیکربندی خود را بارگذاری کرده است. این endpoint سلامتِ خودِ oauth2-proxy است و هرگز درخواست نشست (session) نمی‌کند. اگر پاسخی دریافت نکردید، journalctl -u oauth2-proxy -n 50 را بخوانید؛ زیرا یک issuer URL اشتباه یا یک cookie secret با طول نادرست، هر دو باعث شکست در زمان راه‌اندازی می‌شوند و هر دو مورد در لاگ ذکر می‌شوند.

ثبت URI تغییر مسیر (Redirect URI) در سرویس‌دهنده

یک برنامه OIDC در سرویس‌دهنده خود ایجاد کنید و URI تغییر مسیر آن را دقیقاً برابر با redirect_url موجود در فایل پیکربندی قرار دهید: https://app.example.com/oauth2/callback. منظور از «دقیق»، مطابقت کامل طرح (scheme)، میزبان (host)، پورت و مسیر (path) کاراکتر به کاراکتر است. وجود یک اسلش اضافه در انتها، آن را به یک URI متفاوت تبدیل می‌کند.

این مورد رایج‌ترین عامل شکست در کل فرآیند راه‌اندازی است و پیش از آنکه oauth2-proxy حتی درگیر شود، رخ می‌دهد. سرویس‌دهنده درخواست احراز هویت را رد کرده و صفحه خطای اختصاصی خود را نمایش می‌دهد، بنابراین هیچ موردی در لاگ oauth2-proxy ثبت نمی‌شود. نشانه این وضعیت در نوار آدرس مرورگر مشخص است: مرورگر همچنان روی دامنه سرویس‌دهنده باقی مانده و رشته پرس‌وجو (query string) حاوی error=invalid_request است یا مستقیماً نام صفحات redirect_uri را نشان می‌دهد. در چنین شرایطی، رکورد برنامه را در پنل سرویس‌دهنده اصلاح کنید، نه در پیکربندی پروکسی.

به‌جای تایپ دستی، URL صادرکننده (issuer URL) را از سرویس‌دهنده کپی کنید. oauth2-proxy عبارت /.well-known/openid-configuration را به oidc_issuer_url اضافه کرده و سند discovery را در زمان شروع به کار فراخوانی می‌کند. ابتدا خودتان آن را بررسی کنید:

curl -s https://id.example.com/application/o/myapp/.well-known/openid-configuration | head -c 400

وجود یک کلید authorization_endpoint در خروجی JSON به این معناست که URL صادرکننده صحیح است. دریافت خطای 404 یا یک صفحه خطای HTML نشان‌دهنده نادرست بودن آن است و oauth2-proxy نیز هنگام شروع به کار با همان خطای 404 متوقف خواهد شد. اگر هنوز سرویس‌دهنده‌ای انتخاب نکرده‌اید، مقایسه Keycloak، Authentik و Zitadel مزایا و معایب هرکدام را بررسی کرده است و اجرای Authentik به عنوان سرور SSO شخصی مراحل مربوط به سمت سرویس‌دهنده را در همین سناریو آموزش می‌دهد.

Nginx: auth_request

Nginx با استفاده از auth_request احراز هویت پیش‌رو (forward auth) را انجام می‌دهد؛ این قابلیت یک زیر-درخواست (sub-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 خالی، مانع از کپی شدن بدنه (body) تمام درخواست‌های POST توسط Nginx به زیر-درخواست می‌شود؛ این موضوع حیاتی است زیرا oauth2-proxy هیچ بخشی از آن را نمی‌خواند. در هنگام آپلود فایل، تنظیمات پیش‌فرض باعث ارسال دوبرابری فایل می‌شود.

جفت auth_request_set $auth_cookie و add_header Set-Cookie، کوکی نشست (session cookie) تازه‌سازی‌شده را به مرورگر بازمی‌گردانند. اگر این موارد حذف شوند، cookie_refresh بی‌سروصدا هیچ کاری انجام نمی‌دهد، زیرا Nginx هدرهای Set-Cookie زیر-درخواست را نادیده می‌گیرد و مرورگر تا زمان انقضای نشست، مقدار قدیمی را حفظ می‌کند.

error_page 401 = @oauth2_signin همان دستوری است که بررسی ناموفق را به یک صفحه ورود تبدیل می‌کند. بدون آن، بازدیدکننده احراز هویت‌نشده با یک صفحه 401 Authorization Required ساده مواجه می‌شود و راهی برای ادامه نخواهد داشت.

همیشه پیش از reload کردن، پیکربندی را تست کنید:

sudo nginx -t && sudo systemctl reload nginx

اگر دستورالعمل‌های پیرامونی برای شما جدید هستند، ساختار پیکربندی reverse proxy در nginx لایه زیرین این مبحث را پوشش می‌دهد.

Traefik: میان‌افزار forwardAuth

Traefik برای انجام این کار به دو میان‌افزار نیاز دارد. یکی از آن‌ها بررسی را انجام می‌دهد و دیگری کد 401 را به یک redirect مرورگر تبدیل می‌کند.

# 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

هر دو را به router که در جلوی برنامه شما قرار دارد متصل کنید و oauth2-proxy را روی router اختصاصی خود در oauth.example.com منتشر کنید، زیرا مرورگر باید بدون عبور از بررسی، به /oauth2/sign_in و /oauth2/callback دسترسی داشته باشد.

statusRewrites نگاشت 401 به 302 بخشی است که معمولاً نادیده گرفته می‌شود. بدون این کار، Traefik پاسخ redirect ورود را با وضعیت 401 برمی‌گرداند، مرورگر آن را دنبال نمی‌کند و بازدیدکننده صفحه‌ای را می‌بیند که فقط شامل کلمه Found. است.

trustForwardHeader: true میزبان و URI اصلی را به oauth2-proxy ارسال می‌کند؛ این ابزار برای ساخت مقدار rd که کاربر را به صفحه درخواستی بازمی‌گرداند، به آن‌ها نیاز دارد. مقدار whitelist_domains را طوری تنظیم کنید که این میزبان را نیز پوشش دهد، در غیر این صورت oauth2-proxy پارامتر rd را به دلیل ریسک open-redirect حذف می‌کند و همه کاربران پس از ورود به / هدایت می‌شوند. یک سرور Traefik معمولاً چندین برنامه را به‌طور هم‌زمان پشتیبانی می‌کند و مسیریابی چندین برنامه Docker Compose از طریق یک نمونه Traefik ساختار router مورد نیاز برای این پیکربندی را نشان می‌دهد.

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 است، زیرا کاربری که وارد سیستم نشده است باید بتواند به مسیرهای ورود (sign-in) و callback دسترسی داشته باشد. اگر بررسی را پیش از این مسیرها قرار دهید، فرآیند ورود تا زمانی که مرورگر از ادامه کار منصرف شود، به خودش redirect خواهد شد.

copy_headers همان چیزی است که هویت را به درخواست upstream منتقل می‌کند و تنها زمانی مقادیر را تولید می‌کند که oauth2-proxy با set_xauthrequest = true اجرا شده باشد. انتخاب بین پروکسی‌ها موضوعی جداگانه است و مقایسه nginx، Caddy و Traefik به بررسی آن می‌پردازد.

چرا ورود به سیستم به صفحه ورود بازمی‌گردد؟

شما در ارائه‌دهنده (provider) وارد می‌شوید، آن شما را بازمی‌گرداند و 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 مخفف cross-site request forgery است و این کوکی وجود دارد تا یک callback بتواند به ورودی که آن را آغاز کرده، متصل شود. مرورگر همین شکست را به شکل زیر می‌بیند:

Login Failed: Unable to find a valid CSRF token. Please try again.

این چهار علت را به ترتیب بررسی کنید.

  1. cookie_secure = true در حالی که مرورگر از طریق HTTP ساده به سایت رسیده است. مرورگر کوکی‌ای که با Secure علامت‌گذاری شده را روی یک مبدأ http:// ذخیره نمی‌کند، بنابراین هرگز بازگردانده نمی‌شود. TLS (امنیت لایه انتقال) را به‌درستی terminate کنید یا cookie_secure = false را فقط هنگام تست روی localhost تنظیم کنید.
  2. مقدار cookie_domains که نام میزبان (hostname) موجود در نوار آدرس را پوشش نمی‌دهد. .example.com شامل app.example.com می‌شود و برای app.example.net هیچ کاری انجام نمی‌دهد.
  3. مرورگر کوکی را حذف می‌کند. یک افزونه حریم خصوصی سخت‌گیرانه یا مسدودسازی کوکی‌های شخص ثالث می‌تواند _oauth2_proxy_csrf را بین redirect خروجی و callback حذف کند.
  4. اختلاف ساعت (Clock drift). اگر ساعت سرور با ساعت ارائه‌دهنده فاصله زیادی داشته باشد، iat و exp توکن ID خارج از بازه پذیرفته‌شده قرار می‌گیرند و نشست (session) در بدو ورود رد می‌شود. timedatectl باید System clock synchronized: yes را گزارش دهد.

به‌جای حدس زدن، وقوع آن را از سمت سرور مشاهده کنید:

sudo journalctl -u oauth2-proxy -f

برنامه را در یک پنجره خصوصی (private window) بارگذاری کنید. هر درخواست با وضعیت خود ثبت می‌شود، بنابراین یک callback که بلافاصله با یک redirect دیگر به سمت ارائه‌دهنده دنبال شود، همان حلقه است که در لاگ ثبت شده است.

مسیرهایی که باید از ورود به سیستم صرف‌نظر کنند: APIها، وب‌هوک‌ها و وب‌سوکت‌ها

احراز هویت پیش‌رو (Forward auth) فرض را بر این می‌گذارد که مرورگر دارای کوکی است. فراخواننده‌های بدون مرورگر با مشکل مواجه می‌شوند.

یک کلاینت API که Authorization: Bearer <token> ارسال می‌کند، کوکی ندارد؛ بنابراین یک پاسخ 302 به صفحه ورود ارائه‌دهنده شما دریافت می‌کند و سپس سعی می‌کند HTML را به عنوان JSON تجزیه کند. دو راه حل تمیز وجود دارد. تنظیم skip_jwt_bearer_tokens = true باعث می‌شود oauth2-proxy یک توکن حامل JWT (JSON web token) معتبر از همان صادرکننده را بپذیرد، که وقتی کلاینت‌های API شما از قبل توکن‌ها را از ارائه‌دهنده دریافت می‌کنند، گزینه درستی است. در غیر این صورت، مسیر را مستثنی کنید:

skip_auth_routes = [
  "^/api/",
  "POST=^/webhook/",
  "GET=^/healthz$"
]

هر مقدار یک عبارت منظم (regular expression) است که با مسیر نرمال‌شده مطابقت داده می‌شود و به‌صورت اختیاری با یک متد HTTP و = پیشوند می‌گیرد. POST=^/webhook/ گیرنده وب‌هوک را برای POST باز می‌گذارد، در حالی که یک انسان که به همان مسیر مرور می‌کند همچنان به صفحه ورود هدایت می‌شود. هر ورودی یک حفره در دروازه شماست، بنابراین عبارت‌ها را با ^ لنگر بزنید و آن‌ها را تا حد امکان محدود نگه دارید.

وب‌سوکت‌ها موردی هستند که افراد در آن اشتباه می‌کنند. درخواست ارتقا (upgrade request) یک GET معمولی HTTP است که همان کوکی‌های سایر درخواست‌ها را حمل می‌کند، بنابراین به‌طور عادی از بررسی عبور می‌کند و نیازی به معافیت ندارد. آنچه باعث خرابی می‌شود، پروکسی کردن پیرامون آن است. بدون هدرهای Upgrade و Connection در موقعیت محافظت‌شده، ارتقا هرگز کامل نمی‌شود و کلاینت برنامه با پیام WebSocket connection ... failed در کنسول مرورگر، برای همیشه تلاش مجدد می‌کند. مستثنی کردن مسیر در اینجا هیچ چیزی را اصلاح نمی‌کند، زیرا درخواست قبلاً احراز هویت شده است.

یک محدودیت واقعی اعمال می‌شود. بررسی فقط یک بار، در هنگام ارتقا انجام می‌شود. وب‌سوکتی که ساعت‌ها باز می‌ماند، هرگز دوباره بررسی نمی‌شود؛ بنابراین حذف یک کاربر در ارائه‌دهنده شما، سوکتی را که در حال حاضر در اختیار دارد نمی‌بندد. برای قطع اتصالات زنده، برنامه را ری‌استارت کنید.

ذخیره‌سازی نشست و کوکی‌هایی که بیش از حد بزرگ می‌شوند

به‌طور پیش‌فرض، کل نشست درون کوکی قرار می‌گیرد و با cookie_secret رمزنگاری می‌شود. این کار باعث می‌شود oauth2-proxy بدون وضعیت (stateless) باقی بماند و به هیچ سرویس اضافی نیاز نداشته باشد. این روش یک سقف محدودیت دارد، زیرا مرورگرها کوکی را در حدود 4 KB محدود می‌کنند. هنگامی که ID token شامل لیست طولانی از ادعاهای گروهی (group claims) باشد، 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 از دسترس خارج شود، تمام نشست‌ها نامعتبر شده و همه کاربران همزمان از سیستم خارج می‌شوند. ذخیره‌ساز کوکی نیز هزینه خاص خود را دارد؛ به این صورت که دو درخواست که همزمان در حال تازه‌سازی یک نشست هستند، ممکن است با هم تداخل پیدا کرده و کاربر را مجبور به ورود مجدد کنند.

آنچه Forward Auth برای شما فراهم نمی‌کند

این مکانیزم مانند یک دروازه در ورودی عمل می‌کند. این ابزار به معنای مدیریت مجوزها (Authorization) در داخل خودِ برنامه نیست و همین تفاوت تعیین می‌کند که آیا این رویکرد برای مورد استفاده شما مناسب است یا خیر.

هنگامی که کاربر از دروازه عبور می‌کند، برنامه همان چیزی را می‌بیند که همیشه می‌دیده است. اگر برنامه دارای نقش‌های کاربری داخلی باشد، Forward Auth آن‌ها را پر نمی‌کند، مگر اینکه برنامه از احراز هویت مبتنی بر هدر (header-based authentication) پشتیبانی کند و یک هدر را به یک حساب کاربری نگاشت کند. Grafana از طریق تنظیمات auth.proxy این کار را انجام می‌دهد. اکثر برنامه‌های self-hosted چنین قابلیتی ندارند، بنابراین هر کسی که از دروازه عبور می‌کند، از نظر برنامه یک هویت واحد است و این هویت اغلب به عنوان مدیر (admin) شناخته می‌شود.

این روش همچنین از توکن‌های API خودِ برنامه محافظت نمی‌کند. یک توکن دسترسی شخصی (personal access token) که توسط برنامه صادر شده، برای احراز هویت در خودِ برنامه معتبر است، نه در oauth2-proxy؛ بنابراین به محض اینکه دروازه در مقابل برنامه قرار می‌گیرد، توکن از کار می‌افتد. برای بازگرداندن عملکرد توکن، باید مسیر API را از احراز هویت مستثنی کنید، اما در این صورت آن توکن تنها محافظ آن مسیر خواهد بود. در این حالت شما دو سیستم احراز هویت متفاوت را روی یک سرویس اجرا می‌کنید و SSO تنها یکی از آن‌ها را پوشش می‌دهد.

ابطال دسترسی (Revocation) سومین شکاف امنیتی است. حذف یک کاربر در سرویس‌دهنده (provider) شما، از ورودهای جدید جلوگیری می‌کند و باعث توقف بازنشانی توکن توسط cookie_refresh می‌شود، اما کوکی نشست (session cookie) موجود تا زمان انقضا معتبر باقی می‌ماند. مقدار پیش‌فرض cookie_expire برابر با 168 ساعت است که به معنای یک هفته دسترسی برای کسی است که همین الان او را حذف کرده‌اید. مقدار cookie_refresh را روی زمان کوتاهی مانند یک ساعت تنظیم کنید تا ابطال دسترسی در آن بازه زمانی اعمال شود.

ردپای حسابرسی (audit trail) نیز در همان دروازه متوقف می‌شود. oauth2-proxy ثبت می‌کند که چه کسی و در چه زمانی عبور کرده است، اما برنامه تنها یک نشست بدون نام را ثبت می‌کند. اگر نیاز دارید پاسخ دهید که چه کسی یک تنظیمات را تغییر داده است، هویت مبتنی بر هدر در برنامه حداقلِ نیاز است و داشتن حساب‌های کاربری واقعی برای هر فرد، پاسخ دقیق و صحیح خواهد بود.

چه زمانی پرداخت هزینه SSO انتخاب بهتری است

Forward auth ابزار مناسبی است زمانی که یک برنامه اصلاً سیستم ورود ندارد یا از یک رمز عبور مشترک استفاده می‌کند و شما می‌خواهید یک نقطه واحد برای افزودن و حذف کاربران داشته باشید. این کار یک بعدازظهر زمان می‌برد و به یک پردازش اضافی نیاز دارد، اما با هر برنامه‌ای که از پروتکل HTTP پشتیبانی کند، کار می‌کند.

این ابزار انتخاب اشتباهی است زمانی که افراد مختلف به سطوح دسترسی متفاوتی در داخل همان برنامه نیاز دارند. یک دروازه (gate) نمی‌تواند قوانینی مانند «آنا می‌تواند داشبوردها را ویرایش کند و بو فقط می‌تواند آن‌ها را بخواند» را اعمال کند. اگر فروشنده یک نسخه SSO ارائه می‌دهد، نگاشت گروه به نقش (group-to-role mapping) معمولاً همان چیزی است که در واقع بابت آن هزینه می‌پردازید؛ بازسازی این قابلیت با استفاده از هدرها و قوانین پروکسی، شکننده‌تر از خرید آن است. مطالعه الگوی قیمت‌گذاری پشت آن نسخه‌های SSO پیش از تصمیم‌گیری در هر دو حالت، ارزشمند است.

دو وضعیت دیگر نیز همین مسیر را پیشنهاد می‌کنند. در کارهای مربوط به انطباق (Compliance) که به سوابق حسابرسی (audit records) به ازای هر کاربر در داخل برنامه نیاز دارند، لاگ دسترسی پروکسی به عنوان مدرک پذیرفته نمی‌شود. همچنین، هر برنامه‌ای که دارای کلاینت موبایل یا دسکتاپ باشد و کوکی‌های مرورگر را حمل نکند، در هر درخواست با این دروازه دچار مشکل خواهد شد.

FAQ

احراز هویت پیش‌رو (forward auth) چیست؟

احراز هویت پیش‌رو الگویی است که در آن reverse proxy پیش از ارسال هر درخواست به سرویس مقصد، آن را از یک سرویس احراز هویت جداگانه استعلام می‌کند. پروکسی هدرهای درخواست را به یک endpoint مانند /oauth2/auth ارسال کرده و کد وضعیت را می‌خواند. کد 202 به معنای مجاز بودن است، بنابراین درخواست اصلی به سمت برنامه هدایت می‌شود. کد 401 به معنای نبود نشست (session) است، بنابراین پروکسی مرورگر را به صفحه ورود هدایت می‌کند. Nginx این قابلیت را با دستور auth_request، Traefik با میان‌افزار forwardAuth و Caddy با forward_auth پیاده‌سازی می‌کنند.

چرا oauth2-proxy من را در یک حلقه به صفحه ورود بازمی‌گرداند؟

درخواست callback بدون کوکی CSRF به oauth2-proxy رسیده است، بنابراین 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 ساده به آن دسترسی پیدا کرده، زیرا مرورگر یک کوکی Secure را روی یک مبدأ http:// ذخیره نمی‌کند. دلیل رایج بعدی، مقدار cookie_domains است که نام میزبان (hostname) موجود در نوار آدرس را پوشش نمی‌دهد.

چگونه به یک کلاینت API یا webhook اجازه عبور از oauth2-proxy را بدهم؟

از skip_auth_routes با یک عبارت منظم (regular expression) محدودشده استفاده کنید که در صورت نیاز به یک متد HTTP خاص محدود می‌شود؛ برای مثال POST=^/webhook/. اگر کلاینت‌های API شما از قبل دارای JWTهایی هستند که توسط همان ارائه‌دهنده صادر شده، skip_jwt_bearer_tokens = true آن توکن‌ها را به جای کوکی می‌پذیرد و مسیر را محافظت‌شده نگه می‌دارد. هر چیزی که در skip_auth_routes لیست شود برای همه بدون احراز هویت در دسترس خواهد بود، بنابراین هر عبارت را تا حد امکان محدود نگه دارید.

آیا oauth2-proxy مجوزهای کاربری را به برنامه ارائه می‌دهد؟

خیر. این یک دروازه است، نه یک سیستم تعیین سطح دسترسی. این ابزار تصمیم می‌گیرد چه کسی به برنامه دسترسی پیدا کند، و هر کسی که به برنامه می‌رسد برای آن برنامه یکسان به نظر می‌رسد، مگر اینکه آن برنامه هدرهای هویتی را بخواند و آن‌ها را به حساب‌های کاربری نگاشت کند. Grafana می‌تواند این کار را از طریق تنظیمات auth.proxy انجام دهد. اکثر برنامه‌های self-hosted نمی‌توانند این کار را انجام دهند، بنابراین هر فردی که از دروازه عبور می‌کند، از همان هویت واحدی استفاده می‌کند که برنامه با آن در حال اجراست.

آیا کسی می‌تواند با تنظیم دستی هدر هویت، oauth2-proxy را دور بزند؟

بله، اگر بتواند مستقیماً به برنامه متصل شود. هویت به صورت یک هدر ساده مانند X-Auth-Request-Email ارسال می‌شود و برنامه به هر چیزی که دریافت می‌کند اعتماد می‌کند. هر کسی که بتواند اتصالی به پورت برنامه برقرار کند، می‌تواند آن هدر را ارسال کرده و به هر کاربری تبدیل شود. برنامه را روی 127.0.0.1 bind کنید یا آن را در یک شبکه داخلی Docker بدون پورت منتشرشده نگه دارید و این موضوع را با sudo ss -tlnp تایید کنید.

#oauth2-proxy#sso#oidc#reverse-proxy#forward-auth