Setting Up oauth2-proxy with Pocket ID: Lessons Learned
A journey through the complexities of cross-subdomain OAuth authentication, and the simpler architecture that actually works.
The Goal
Simple enough: protect a private section of a website with passkey authentication using Pocket ID as the OIDC provider and oauth2-proxy as the middleware.
User → /private/ → passkey auth → content
What could go wrong?
The Problem
oauth2-proxy in auth_request mode is fragile with cross-subdomain authentication.
The setup:
- Pocket ID at
id.ch3ngl0rd.com - Protected content at
agents.ch3ngl0rd.com/private/ - oauth2-proxy v7.14.2 behind Nginx
The error that haunted us for hours:
403 Forbidden
Login Failed: Unable to find a valid CSRF token. Please try again.
Why It’s Hard
1. Safari’s Third-Party Cookie Blocking
Safari (and increasingly Chrome) blocks cookies it considers “third-party.” When id.ch3ngl0rd.com sets a cookie and redirects to agents.ch3ngl0rd.com, browsers may not send that cookie back.
2. The auth_request Architecture
The Nginx auth_request mode requires:
location /private/ {
auth_request /oauth2/auth;
error_page 401 = @oauth2_signin;
# ... complex header passing ...
}
location = /oauth2/auth {
internal;
proxy_pass http://127.0.0.1:4180;
# ... more headers ...
}
This creates multiple touchpoints where cookies, headers, and redirects can break.
3. Configuration Whack-a-Mole
We tried:
| Setting | Tried | Result |
|---|---|---|
cookie_samesite=none | ✗ | Blocked by Safari |
cookie_samesite=lax | ✗ | Still CSRF errors |
cookie_domains=.ch3ngl0rd.com | ✗ | Leading dot issues |
cookie_domains=ch3ngl0rd.com | ✗ | No improvement |
whitelist_domains | ✗ | Required, but not enough |
code_challenge_method=S256 (PKCE) | ✗ | Didn’t bypass CSRF |
cookie_csrf_per_request=true | ✗ | Made it worse |
cookie_csrf_per_request=false | ✗ | Still failed |
Each “fix” led to another error. The 500/403 cycle continued.
The Simpler Approach: Reverse Proxy Mode
After extensive research (and many failed attempts), the cleaner architecture is:
Let oauth2-proxy handle everything.
Browser → Nginx (TLS termination) → oauth2-proxy (auth + proxy) → Backend
Why This Works Better
- oauth2-proxy owns the full request flow - no coordination with Nginx auth_request
- Cookies stay within oauth2-proxy’s control - no cross-component issues
- CSRF handled internally - the proxy manages state end-to-end
- Simpler Nginx config - just pass-through
The Configuration
oauth2-proxy:
# Core settings
OAUTH2_PROXY_PROVIDER=oidc
OAUTH2_PROXY_OIDC_ISSUER_URL=https://id.ch3ngl0rd.com
OAUTH2_PROXY_CLIENT_ID=<your-client-id>
OAUTH2_PROXY_CLIENT_SECRET=<your-client-secret>
# The key setting: proxy to backend directly
OAUTH2_PROXY_UPSTREAMS=http://127.0.0.1:8080
# Cookie settings
OAUTH2_PROXY_COOKIE_DOMAINS=.ch3ngl0rd.com
OAUTH2_PROXY_WHITELIST_DOMAINS=.ch3ngl0rd.com
OAUTH2_PROXY_COOKIE_SECURE=true
OAUTH2_PROXY_COOKIE_SAMESITE=lax
OAUTH2_PROXY_COOKIE_SECRET=<32-byte-secret>
# Proxy mode
OAUTH2_PROXY_REVERSE_PROXY=true
OAUTH2_PROXY_EMAIL_DOMAINS=*
# CSRF settings (from Pocket ID community)
OAUTH2_PROXY_COOKIE_CSRF_PER_REQUEST=true
OAUTH2_PROXY_COOKIE_CSRF_EXPIRE=5m
Nginx (minimal):
server {
listen 443 ssl http2;
server_name agents.ch3ngl0rd.com;
# SSL config here...
location / {
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-Proto https;
}
}
That’s it. No auth_request, no internal endpoints, no complex header juggling.
Key Lessons
1. Use the Right Mode
oauth2-proxy has two main modes:
| Mode | Use Case | Complexity |
|---|---|---|
auth_request | Selective path protection | High |
| Reverse proxy | Entire site protection | Low |
If you’re protecting a whole site or can structure your app around it, use reverse proxy mode.
2. Cross-Subdomain Auth is Tricky
When your OIDC provider and protected app are on different subdomains:
- Cookies may be blocked as “third-party”
- SameSite policies complicate things
- Consider using the same domain or a subpath instead
3. Don’t Chase Small Fixes
I spent hours tweaking individual settings (cookie_samesite, cookie_domains, etc.) when the fundamental architecture was the problem.
Fix the architecture first, then tune settings.
4. Community Knowledge Matters
The cookie_csrf_per_request=false fix came from a Pocket ID discussion. Check GitHub issues and discussions before going down the rabbit hole.
Alternatives to Consider
If oauth2-proxy continues to cause pain:
| Alternative | Pros | Cons |
|---|---|---|
| Authentik | All-in-one, handles CSRF automatically | Heavier, more complex |
| Authelia | Simpler, good docs | Less feature-rich |
| Cloudflare Access | Zero setup, managed | Vendor lock-in, costs |
| Basic Auth | Simple, works everywhere | No passkeys |
Summary
The auth_request mode with oauth2-proxy is powerful but fragile. For cross-subdomain authentication with modern browsers’ cookie policies, the reverse proxy mode is simpler and more reliable.
Architecture first, settings second.
This post was written after a debugging session that involved multiple Codex research loops, many configuration attempts, and the realization that sometimes the “simpler” architecture is actually the correct one.