oauth2-proxy ব্যবহার করে যেকোনো অ্যাপে SSO যুক্ত করার নিয়ম
oauth2-proxy এবং forward auth ব্যবহার করে লগইনহীন অ্যাপে SSO যুক্ত করুন। Nginx বা Traefik-এর মাধ্যমে OIDC ইন্টিগ্রেশন করার সময় cookie ও redirect সংক্রান্ত জটিলতা এড়ানোর উপায় জানুন।
Forward auth: যে অ্যাপে লগইন নেই তাতে SSO যুক্ত করা
oauth2-proxy এমন একটি অ্যাপ্লিকেশনে single sign-on সুবিধা দেয় যার নিজস্ব কোনো লগইন ব্যবস্থা নেই। এটি কার্যকর হয় কারণ অ্যাপ্লিকেশনের সামনে থাকা reverse proxy প্রতিটি অনুরোধ আটকে দেয় এবং oauth2-proxy-কে জিজ্ঞাসা করে যে অনুরোধটিতে কোনো বৈধ session আছে কি না। শুধুমাত্র উত্তর 'হ্যাঁ' হলে proxy অনুরোধটিকে upstream-এ পাঠায়। অ্যাপ্লিকেশন কোডে কোনো পরিবর্তন করতে হয় না, কারণ অ্যাপ্লিকেশনটি কখনোই এই যাচাইকরণ প্রক্রিয়াটি দেখতে পায় না।
এই যাচাইকরণটি একটি অতিরিক্ত HTTP অনুরোধ। proxy আগত অনুরোধের header-এর একটি কপি /oauth2/auth-এ পাঠায় এবং status code পড়ে। 202 মানে হলো ব্যবহারকারীর একটি session আছে, তাই proxy মূল অনুরোধটিকে অ্যাপের কাছে পাঠিয়ে দেয়। 401 মানে হলো কোনো session নেই, তাই proxy ব্রাউজারটিকে /oauth2/sign_in-এ পাঠিয়ে দেয়, যা আপনার identity provider-এ একটি OpenID Connect (OIDC) লগইন প্রক্রিয়া শুরু করে। OIDC হলো OAuth 2.0-এর ওপর ভিত্তি করে তৈরি একটি identity layer এবং provider হলো সেটিই যা আপনি লগইনের জন্য ইতিমধ্যে ব্যবহার করছেন।
প্রতিটি reverse proxy-তে এই প্যাটার্নটির একটি নির্দিষ্ট নাম আছে। Nginx-এ এই directive-কে বলা হয় auth_request। Traefik-এ একে বলা হয় middleware forwardAuth। Caddy-তে এটি লেখা হয় forward_auth হিসেবে। sub-request-এর উত্তর দেওয়া service-টিও পরিবর্তনযোগ্য। oauth2-proxy একটি সাধারণ পছন্দ কারণ এটি সরাসরি OIDC সমর্থন করে এবং এর নিজস্ব কোনো database-এর প্রয়োজন হয় না।
কনফিগারেশন লেখার আগেই ট্রাস্ট বাউন্ডারি নির্ধারণ করুন
সফলভাবে চেক সম্পন্ন হওয়ার পর, oauth2-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/ সরাসরি অ্যাপে পৌঁছাতে পারলে তা পুরো সিকিউরিটি ব্যবস্থাকে বাইপাস করে ফেলে।
তাই প্রক্সি ছাড়া অ্যাপ্লিকেশনটি যেন অন্য কোনোভাবে অ্যাক্সেস করা না যায়, তা নিশ্চিত করতে হবে। Docker Compose-এ, অ্যাপ সার্ভিস থেকে ports: ম্যাপিংটি মুছে ফেলুন এবং সেটিকে ইন্টারনাল নেটওয়ার্কে রাখুন, যাতে শুধুমাত্র প্রক্সি কন্টেইনারই সেটির সাথে যোগাযোগ করতে পারে। বেয়ার হোস্টের ক্ষেত্রে, অ্যাপটিকে 0.0.0.0:3000-এর পরিবর্তে 127.0.0.1:3000-এ বাইন্ড করুন। এরপর আপনি আসলে কী এক্সপোজ করেছেন তা যাচাই করুন:
sudo ss -tlnp | grep 3000যদি কোনো লাইনে 0.0.0.0:3000 লেখা থাকে, তার মানে অ্যাপটি পাবলিক IP-তে সাড়া দিচ্ছে এবং আপনার গেটওয়েটি কেবল লোক দেখানো। 127.0.0.1:3000 হলো আপনার কাঙ্ক্ষিত অবস্থা। ফায়ারওয়াল রুল একটি কার্যকর দ্বিতীয় স্তর হিসেবে কাজ করে, কিন্তু বাইন্ড অ্যাড্রেসটি এমন একটি বিষয় যা অন্য কোনো টুল আপনার রুলসেট মুছে ফেললেও টিকে থাকে।
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 --versionsha256sum -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 (advanced encryption standard) কি হিসেবে কাজ করে। অন্য যেকোনো দৈর্ঘ্যের হলে oauth2-proxy চালু হতে অস্বীকার করবে এবং স্টার্টআপ এররে কুকি সিক্রেটের নাম উল্লেখ করবে।
openssl rand -base64 32 | tr -- '+/' '-_'tr কেবল সাজসজ্জার জন্য নয়। এটি স্ট্যান্ডার্ড base64-কে URL-safe অ্যালফাবেটে রূপান্তর করে, যাতে মানটি কোনো উদ্ধৃতি চিহ্ন (quoting) সংক্রান্ত সমস্যা ছাড়াই শেল, env ফাইল এবং HTTP হেডারে টিকে থাকতে পারে।
এই মানের জন্য দুটি নিয়ম রয়েছে। প্রতিটি ডেপ্লয়মেন্টের জন্য আলাদা সিক্রেট ব্যবহার করুন। আর যখন আপনি একই ডোমেইনের অধীনে একাধিক oauth2-proxy ইনস্ট্যান্স চালাবেন, তখন সেগুলোকে একই সিক্রেট দিন, কারণ একটি ইনস্ট্যান্সের এনক্রিপ্ট করা কুকি অন্যগুলোর দ্বারা পাঠযোগ্য হতে হবে।
oauth2-proxy কনফিগারেশন লিখুন
দীর্ঘ কমান্ড লাইনের পরিবর্তে সেটিংসগুলো একটি ফাইলে রাখুন, যাতে ps আউটপুটে client secret কখনোই দেখা না যায়।
# /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-এর জন্য ঠিক এটাই প্রয়োজন, কারণ রিভার্স প্রক্সি নিজেই প্রক্সিংয়ের কাজটি করে। অন্য ডিপ্লয়মেন্ট মডেলে 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/ping প্রিন্ট হওয়া মানে OK প্রসেসটি শুরু হয়েছে এবং কনফিগারেশন লোড করেছে। এটি oauth2-proxy-এর নিজস্ব হেলথ এন্ডপয়েন্ট এবং এটি কখনোই সেশনের জন্য অনুরোধ করে না। যদি কোনো উত্তর না পাওয়া যায়, তবে journalctl -u oauth2-proxy -n 50 পড়ুন, কারণ ভুল issuer URL এবং ভুল দৈর্ঘ্যের cookie secret উভয়ই স্টার্টআপের সময় ব্যর্থ হয় এবং উভয় ক্ষেত্রেই তা লগ ফাইলে উল্লেখ থাকে।
আপনার প্রোভাইডারের সাথে redirect URI নিবন্ধন করুন
আপনার প্রোভাইডারে একটি OIDC অ্যাপ্লিকেশন তৈরি করুন এবং এর redirect URI-কে কনফিগারেশনের redirect_url-এর সাথে হুবহু মিলিয়ে সেট করুন: https://app.example.com/oauth2/callback। হুবহু মিল বলতে বোঝায় scheme, host, port এবং path প্রতিটি অক্ষর যেন একই থাকে। একটি trailing slash থাকলে সেটি ভিন্ন URI হিসেবে গণ্য হয়।
পুরো সেটআপের মধ্যে এটিই সবচেয়ে সাধারণ ব্যর্থতার কারণ, এবং oauth2-proxy কাজ শুরু করার আগেই এটি ব্যর্থ হয়। প্রোভাইডার অথোরাইজেশন রিকোয়েস্টটি প্রত্যাখ্যান করে এবং তাদের নিজস্ব এরর পেজ দেখায়, তাই oauth2-proxy-এর লগে কোনো তথ্য পাওয়া যায় না। এর লক্ষণ হলো ব্রাউজারের অ্যাড্রেস বার: ব্রাউজারটি তখনও আপনার প্রোভাইডারের ডোমেইনেই থাকে এবং query string-এ error=invalid_request অথবা সরাসরি redirect_uri পেজের নাম থাকে। যখন আপনি এটি দেখবেন, তখন প্রোভাইডারের অ্যাপ্লিকেশন রেকর্ডটি ঠিক করুন, প্রক্সি কনফিগারেশন নয়।
issuer URL টাইপ না করে প্রোভাইডার থেকে কপি করুন। oauth2-proxy, oidc_issuer_url-এর সাথে /.well-known/openid-configuration যুক্ত করে এবং স্টার্টআপের সময় সেই discovery document সংগ্রহ করে। প্রথমে আপনি নিজে এটি যাচাই করুন:
curl -s https://id.example.com/application/o/myapp/.well-known/openid-configuration | head -c 400JSON-এর মধ্যে একটি authorization_endpoint কি (key) থাকা মানে হলো issuer URL সঠিক। একটি 404 বা HTML এরর পেজ মানে হলো এটি ভুল, এবং oauth2-proxy সেই একই 404 এর কারণে স্টার্ট হতে ব্যর্থ হবে। আপনি যদি এখনও কোনো প্রোভাইডার নির্বাচন না করে থাকেন, তবে Keycloak, Authentik এবং Zitadel-এর তুলনা বিষয়টি বিভিন্ন দিক নিয়ে আলোচনা করে, এবং আপনার নিজস্ব SSO সার্ভার হিসেবে Authentik চালানো এই সেটআপের প্রোভাইডার অংশটি কীভাবে করতে হয় তা বিস্তারিত দেখায়।
Nginx: auth_request
Nginx, auth_request ব্যবহার করে forward auth পরিচালনা করে, যা একটি অভ্যন্তরীণ sub-request তৈরি করে এবং তার status code-এর ওপর ভিত্তি করে সিদ্ধান্ত নেয়।
# 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 রিকোয়েস্টের বডি sub-request-এ কপি করা বন্ধ করে দেয়। এটি জরুরি কারণ oauth2-proxy বডির কোনো অংশই পড়ে না। ফাইল আপলোডের ক্ষেত্রে, ডিফল্ট কনফিগারেশন ফাইলটিকে দুইবার পাঠায়।
auth_request_set $auth_cookie এবং add_header Set-Cookie জোড়াটি রিফ্রেশ হওয়া session cookie-কে ব্রাউজারে ফেরত পাঠায়। এটি বাদ দিলে cookie_refresh নীরবে কোনো কাজ করে না, কারণ Nginx sub-request-এর Set-Cookie বাতিল করে দেয় এবং সেশন শেষ না হওয়া পর্যন্ত ব্রাউজার পুরনো ভ্যালুই ধরে রাখে।
error_page 401 = @oauth2_signin ব্যর্থ চেককে একটি লগইন পেজে রূপান্তর করে। এটি ছাড়া, একজন অননুমোদিত ভিজিটর শুধু একটি খালি 401 Authorization Required পেজ দেখতে পায় এবং সামনে যাওয়ার কোনো উপায় থাকে না।
রিলোড করার আগে সবসময় পরীক্ষা করুন:
sudo nginx -t && sudo systemctl reload nginxযদি আশেপাশের ডিরেক্টিভগুলো আপনার কাছে নতুন মনে হয়, তবে nginx reverse proxy কনফিগারেশনের গঠন এই স্তরের নিচের বিষয়গুলো বিস্তারিত আলোচনা করে।
Traefik: forwardAuth middleware
Traefik-এ একই কাজের জন্য দুটি middleware প্রয়োজন। একটি চেক সম্পন্ন করে। অন্যটি 401 status-কে ব্রাউজার রিডাইরেক্টে রূপান্তর করে।
# 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 status সহ সাইন-ইন রিডাইরেক্ট পাঠায়, ব্রাউজার সেটি অনুসরণ করে না এবং ভিজিটর একটি পেজ দেখতে পায় যেখানে শুধু Found. শব্দটি লেখা থাকে।
trustForwardHeader: true অরিজিনাল হোস্ট এবং URI-কে oauth2-proxy-এর কাছে পাঠিয়ে দেয়, যা ব্যবহারকারীকে তার কাঙ্ক্ষিত পেজে ফিরিয়ে নেওয়ার জন্য rd ভ্যালু তৈরিতে প্রয়োজন হয়। whitelist_domains-কে এমনভাবে সেট করুন যেন তা সেই হোস্টকেও কভার করে, অন্যথায় oauth2-proxy ওপেন-রিডাইরেক্ট ঝুঁকির কারণে rd প্যারামিটারটি বাদ দিয়ে দেয় এবং লগইনের পর সবাই /-এ ল্যান্ড করে। একটি Traefik বক্স সাধারণত একসাথে অনেকগুলো অ্যাপ হ্যান্ডেল করে এবং একটি Traefik ইনস্ট্যান্সের মাধ্যমে একাধিক Docker Compose অ্যাপ রাউট করা বিষয়টি সেই রাউটার লেআউট দেখায় যেখানে এটি যুক্ত করা হয়।
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 থাকে না, কারণ লগ-ইন না করা ভিজিটরকে অবশ্যই সাইন-ইন এবং কলব্যাক পাথগুলোতে পৌঁছাতে দিতে হবে। যদি এই পাথগুলোর আগে চেক বসানো হয়, তবে ব্রাউজার হাল না ছাড়া পর্যন্ত লগ-ইন প্রক্রিয়াটি বারবার নিজেকেই রিডাইরেক্ট করতে থাকবে।
copy_headers আপস্ট্রিম রিকোয়েস্টে পরিচয় বা identity যুক্ত করে এবং এটি কেবল তখনই মান প্রদান করে যখন oauth2-proxy set_xauthrequest = true ফ্ল্যাগসহ চলে। কোন প্রক্সি বেছে নেবেন তা একটি আলাদা বিষয়, এবং nginx, Caddy এবং Traefik-এর তুলনা বিষয়টি বিস্তারিত আলোচনা করে।
লগইন কেন বারবার লগইন পেজেই ফিরে আসে?
আপনি প্রোভাইডারের কাছে সাইন-ইন করেন, এটি আপনাকে ফেরত পাঠায়, এবং oauth2-proxy আপনাকে সরাসরি আবার প্রোভাইডারের কাছে পাঠিয়ে দেয়। এই লুপের অর্থ হলো, কলব্যাক রিকোয়েস্টটি সেই কুকি ছাড়া এসেছে যা 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, এবং এই কুকিটি থাকার কারণ হলো যাতে একটি কলব্যাককে সেই লগইনের সাথে যুক্ত করা যায় যা এটি শুরু করেছিল। ব্রাউজার একই ব্যর্থতাকে এভাবে দেখে:
Login Failed: Unable to find a valid CSRF token. Please try again.এই চারটি কারণ ক্রমানুসারে পরীক্ষা করুন।
cookie_secure = trueযখন ব্রাউজার সাধারণ HTTP-এর মাধ্যমে সাইটটিতে পৌঁছায়। ব্রাউজারSecureচিহ্নিত কোনো কুকিhttp://অরিজিনে সংরক্ষণ করবে না, তাই এটি কখনোই ফেরত পাঠানো হয় না। সঠিকভাবে TLS (transport layer security) টার্মিনেট করুন, অথবা শুধুমাত্র localhost-এ পরীক্ষা করার সময়cookie_secure = falseসেট করুন।- একটি
cookie_domainsভ্যালু যা অ্যাড্রেস বারের হোস্টনামকে কভার করে না।.example.comশুধুমাত্রapp.example.com-কে কভার করে এবংapp.example.net-এর জন্য কোনো কাজই করে না। - ব্রাউজার কুকিটি বাদ দিচ্ছে। কোনো কঠোর প্রাইভেসি এক্সটেনশন বা থার্ড-পার্টি কুকি ব্লকিং আউটবাউন্ড রিডাইরেক্ট এবং কলব্যাকের মধ্যবর্তী সময়ে
_oauth2_proxy_csrfমুছে ফেলতে পারে। - ক্লক ড্রিফট (Clock drift)। সার্ভারের ঘড়ি যদি প্রোভাইডারের ঘড়ি থেকে অনেক পিছিয়ে বা এগিয়ে থাকে, তবে ID টোকেনের
iatএবংexpগ্রহণযোগ্য সময়ের বাইরে চলে যায় এবং আসার সাথে সাথেই সেশনটি বাতিল হয়ে যায়।timedatectl-এর উচিতSystem clock synchronized: yesরিপোর্ট করা।
অনুমান না করে সার্ভার সাইড থেকে এটি পর্যবেক্ষণ করুন:
sudo journalctl -u oauth2-proxy -fএকটি প্রাইভেট উইন্ডোতে অ্যাপটি লোড করুন। প্রতিটি রিকোয়েস্ট তার স্ট্যাটাসসহ লগ করা হয়, তাই একটি কলব্যাকের ঠিক পরেই প্রোভাইডারের কাছে আরেকটি রিডাইরেক্ট হওয়া মানেই হলো লুপ, যা রেকর্ডে ধরা পড়বে।
যেসব পাথ লগইন এড়িয়ে চলবে: API, webhook এবং websocket
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$"
]প্রতিটি ভ্যালু হলো একটি রেগুলার এক্সপ্রেশন, যা নরমালাইজড পাথের সাথে মেলানো হয়। এর আগে ঐচ্ছিকভাবে HTTP মেথড এবং = যোগ করা যায়। POST=^/webhook/ ব্যবহার করলে webhook রিসিভারটি POST-এর জন্য উন্মুক্ত থাকে, কিন্তু একই পাথে ব্রাউজ করা কোনো ব্যবহারকারী ঠিকই লগইন পেজে পৌঁছান। প্রতিটি এন্ট্রি আপনার গেটওয়েতে একটি ছিদ্র তৈরি করে, তাই ^ দিয়ে এক্সপ্রেশনগুলোকে অ্যাঙ্কর করুন এবং ক্লায়েন্টের প্রয়োজন অনুযায়ী সেগুলোকে যতটা সম্ভব সুনির্দিষ্ট রাখুন।
Websocket-এর ক্ষেত্রে মানুষ সাধারণত ভুল করে। আপগ্রেড রিকোয়েস্টটি একটি সাধারণ 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 ডাউন হয়ে যায়, তবে প্রতিটি সেশন অকার্যকর হয়ে যাবে এবং সবাইকে একসাথে লগ আউট করে দেওয়া হবে। কুকি স্টোরের নিজস্ব অসুবিধা রয়েছে, যেমন একই সময়ে দুটি রিকোয়েস্ট একই সেশন রিফ্রেশ করলে কনফ্লিক্ট হতে পারে এবং ব্যবহারকারীকে পুনরায় লগ ইন করতে বাধ্য করতে পারে।
Forward auth যা প্রদান করে না
এটি দরজার মুখে একটি গেট মাত্র। এটি অ্যাপ্লিকেশনের অভ্যন্তরীণ কোনো অনুমোদন (authorisation) নয়, এবং এই পার্থক্যটিই নির্ধারণ করে যে এই পদ্ধতিটি আপনার ক্ষেত্রে উপযুক্ত কি না।
একবার ব্যবহারকারী গেট পার হয়ে গেলে, অ্যাপ্লিকেশনটি সবসময় যা দেখত তা-ই দেখে। যদি অ্যাপটির নিজস্ব রোল বা ভূমিকা থাকে, তবে forward auth সেগুলো পূরণ করে না, যদি না অ্যাপটি header-based authentication সমর্থন করে এবং কোনো হেডারকে অ্যাকাউন্টের সাথে ম্যাপ করতে পারে। Grafana তার auth.proxy সেটিংসের মাধ্যমে এটি করতে পারে। বেশিরভাগ self-hosted অ্যাপ এটি পারে না, তাই গেট পার হওয়া প্রত্যেকেই অ্যাপ্লিকেশনের কাছে একই পরিচয় বহন করে এবং সেই পরিচয়টি প্রায়শই অ্যাডমিনের হয়।
এটি অ্যাপ্লিকেশনের নিজস্ব API টোকেনকেও সুরক্ষিত করে না। অ্যাপ দ্বারা ইস্যু করা একটি personal access token অ্যাপের কাছে প্রমাণ দেয়, oauth2-proxy-এর কাছে নয়, তাই গেটটি সামনে বসানোর সাথে সাথেই টোকেনটি কাজ করা বন্ধ করে দেয়। API পাথটিকে ছাড় দিলে এটি আবার কাজ করবে, কিন্তু তখন সেই টোকেনটিই পাথটিকে রক্ষা করার একমাত্র উপায় হয়ে দাঁড়াবে। আপনি এখন একটি সার্ভিসে দুটি প্রমাণীকরণ ব্যবস্থা চালাচ্ছেন এবং SSO কেবল একটিকেই কভার করছে।
Revocation বা প্রত্যাহার হলো তৃতীয় ঘাটতি। আপনার প্রোভাইডার থেকে কোনো ব্যবহারকারীকে মুছে ফেললে নতুন লগইন বন্ধ হয় এবং cookie_refresh যে টোকেন রিফ্রেশ করে তাও বন্ধ হয়ে যায়, কিন্তু বিদ্যমান session cookie মেয়াদ শেষ না হওয়া পর্যন্ত বৈধ থাকে। cookie_expire ডিফল্টভাবে 168 ঘণ্টা সেট করা থাকে, যার মানে আপনি যাকে মুছে ফেলেছেন সে আরও এক সপ্তাহ অ্যাক্সেস পাবে। cookie_refresh-কে এক ঘণ্টার মতো কম সময়ে সেট করুন, যাতে প্রত্যাহার সেই সময়ের মধ্যেই কার্যকর হয়।
অডিট ট্রেইলও গেটের কাছে এসে থেমে যায়। oauth2-proxy লগ রাখে কে কখন প্রবেশ করেছে। অ্যাপ্লিকেশনটি একটি নামহীন সেশনের লগ রাখে। যদি আপনাকে উত্তর দিতে হয় কে সেটিংস পরিবর্তন করেছে, তবে অ্যাপে হেডার আইডেন্টিটি থাকাটা নূন্যতম প্রয়োজন, আর প্রকৃত per-user অ্যাকাউন্ট থাকাই হলো সঠিক সমাধান।
কখন SSO-এর জন্য অর্থ প্রদান করা ভালো সিদ্ধান্ত
যখন কোনো অ্যাপে লগইন ব্যবস্থা নেই বা সবার জন্য একটিই সাধারণ পাসওয়ার্ড ব্যবহার করা হয়, তখন Forward auth একটি কার্যকর সমাধান। এর মাধ্যমে আপনি একটি কেন্দ্রীয় জায়গা থেকে ব্যবহারকারীদের যোগ বা অপসারণ করতে পারেন। এটি সেটআপ করতে সাধারণত এক বিকেল সময় লাগে এবং একটি অতিরিক্ত প্রসেস চালাতে হয়, তবে এটি HTTP সমর্থনকারী যেকোনো অ্যাপের সাথে কাজ করে।
যখন একই অ্যাপের ভেতরে বিভিন্ন ব্যবহারকারীর জন্য ভিন্ন ভিন্ন অনুমতির প্রয়োজন হয়, তখন এটি সঠিক সমাধান নয়। একটি গেটওয়ে বা প্রক্সি "আনা ড্যাশবোর্ড এডিট করতে পারবে এবং বো শুধু তা পড়তে পারবে"—এই ধরনের জটিল শর্ত পূরণ করতে পারে না। যদি ভেন্ডর SSO-এর জন্য আলাদা কোনো টিয়ার (tier) অফার করে, তবে আপনি মূলত গ্রুপ-টু-রোল ম্যাপিং (group-to-role mapping)-এর জন্যই অর্থ প্রদান করছেন। প্রক্সি রুল বা হেডার ব্যবহার করে এই ব্যবস্থা পুনরায় তৈরি করা বেশ জটিল এবং ঝুঁকিপূর্ণ। সিদ্ধান্ত নেওয়ার আগে SSO টিয়ারের পেছনের মূল্য নির্ধারণের ধরন সম্পর্কে জেনে নেওয়া ভালো।
আরও দুটি পরিস্থিতিতে SSO কেনা যুক্তিযুক্ত। প্রথমত, কমপ্লায়েন্স বা নিরীক্ষার প্রয়োজনে যদি অ্যাপের ভেতরে প্রতি ব্যবহারকারীর আলাদা অডিট রেকর্ড প্রয়োজন হয়, তবে প্রক্সি অ্যাক্সেস লগ প্রমাণ হিসেবে গ্রহণযোগ্য হবে না। দ্বিতীয়ত, যেসব মোবাইল বা ডেস্কটপ ক্লায়েন্ট ব্রাউজার কুকি (browser cookies) বহন করে না, সেসব অ্যাপের ক্ষেত্রে প্রতিটি রিকোয়েস্টে গেটওয়ের সাথে সমস্যা তৈরি হবে।
FAQ
Forward auth কী?
Forward auth হলো এমন একটি প্যাটার্ন যেখানে reverse proxy প্রতিটি incoming request-কে upstream-এ পাঠানোর আগে একটি আলাদা authentication service-এর কাছে যাচাই করে নেয়। proxy অনুরোধের header-গুলো /oauth2/auth-এর মতো একটি endpoint-এ পাঠায় এবং status code পড়ে দেখে। 202 মানে অনুমতি দেওয়া হয়েছে, তাই মূল অনুরোধটি অ্যাপ্লিকেশনে পৌঁছে যায়। 401 মানে কোনো session নেই, তাই proxy ব্রাউজারকে login পেজে redirect করে। Nginx-এ এটি auth_request directive দিয়ে, Traefik-এ forwardAuth middleware দিয়ে এবং Caddy-তে forward_auth দিয়ে বাস্তবায়ন করা হয়।
oauth2-proxy কেন আমাকে বারবার login পেজে redirect করে?
callback request-টি CSRF cookie ছাড়াই oauth2-proxy-তে পৌঁছেছে, তাই oauth2-proxy পুনরায় flow শুরু করে। সার্ভার লগে No cookies were found in OAuth callback. দেখা যায় এবং ব্রাউজারে Login Failed: Unable to find a valid CSRF token. Please try again. প্রদর্শিত হয়। এর সাধারণ কারণ হলো plain HTTP-তে ব্রাউজ করা সাইটে cookie_secure = true, কারণ ব্রাউজার http:// origin-এ কোনো Secure cookie সংরক্ষণ করে না। এর পরের সাধারণ কারণ হলো এমন একটি cookie_domains value যা address bar-এর hostname-কে কভার করে না।
আমি কীভাবে একটি API client বা webhook-কে oauth2-proxy-এর ভেতর দিয়ে যেতে দেব?
skip_auth_routes-এর সাথে একটি anchored regular expression ব্যবহার করুন, যা প্রয়োজনে একটি নির্দিষ্ট HTTP method-এর জন্য সীমাবদ্ধ রাখা যায়, যেমন POST=^/webhook/। যদি আপনার API client-এর কাছে একই provider-এর ইস্যু করা JWT থাকে, তবে skip_jwt_bearer_tokens = true cookie-এর পরিবর্তে সেই token গ্রহণ করে এবং path-টিকে সুরক্ষিত রাখে। skip_auth_routes-এ তালিকাভুক্ত যেকোনো কিছু সবার জন্য unauthenticated থাকে, তাই প্রতিটি expression-কে caller-এর প্রয়োজন অনুযায়ী যতটা সম্ভব সংকীর্ণ রাখুন।
oauth2-proxy কি অ্যাপ্লিকেশনকে per-user permission দেয়?
না। এটি একটি gate, কোনো authorisation system নয়। এটি সিদ্ধান্ত নেয় কে অ্যাপ্লিকেশনে পৌঁছাবে, এবং যে কেউ সেখানে পৌঁছালে অ্যাপ্লিকেশনের কাছে তাদের সবাইকে একই মনে হয়, যদি না অ্যাপ্লিকেশনটি identity header পড়ে সেগুলোকে account-এর সাথে map করে। Grafana তার auth.proxy সেটিংসের মাধ্যমে এটি করতে পারে। অধিকাংশ self-hosted অ্যাপ এটি পারে না, তাই gate পার হওয়া প্রতিটি ব্যক্তি সেই একই identity শেয়ার করে যা দিয়ে অ্যাপটি চলছে।
কেউ কি নিজে identity header সেট করে oauth2-proxy বাইপাস করতে পারে?
হ্যাঁ, যদি তারা সরাসরি অ্যাপ্লিকেশনে পৌঁছাতে পারে। identity একটি plain header হিসেবে আসে, যেমন X-Auth-Request-Email, এবং অ্যাপ্লিকেশন যা পায় তা-ই বিশ্বাস করে। যে কেউ অ্যাপ্লিকেশনের port-এ connection খুলতে পারলে সেই header পাঠিয়ে যেকোনো user সেজে যেতে পারে। অ্যাপটিকে 127.0.0.1-এ bind করুন, অথবা কোনো published port ছাড়া internal Docker network-এ রাখুন এবং sudo ss -tlnp দিয়ে তা নিশ্চিত করুন।