Replace builtin proxy auth with oauth2-proxy#355
Open
runleveldev wants to merge 20 commits into
Open
Conversation
6fe0ce8 to
75c0db0
Compare
runleveldev
commented
Jun 16, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
This PR replaces the manager’s built-in NGINX auth_request forward-auth implementation with an external, administrator-managed oauth2-proxy deployment (one shared instance per parent domain), and updates the NGINX template + docs/UI to match the new contract.
Changes:
- Rewrites the NGINX auth block to use oauth2-proxy’s
/oauth2/authsubrequest and forwards identity to backends via stable headers (X-User,X-Email,X-Groups,X-Access-Token). - Removes the manager’s
/verifyforward-auth endpoint and narrows the manager session cookie scope back to the manager host. - Updates admin/developer docs and external-domain UI to describe and configure
authServeras the public oauth2-proxy URL.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| mie-opensource-landing/docs/developers/system-architecture.md | Updates architecture docs/sequence diagram to describe oauth2-proxy-based auth_request. |
| mie-opensource-landing/docs/developers/database-schema.md | Re-describes ExternalDomain.authServer as public oauth2-proxy URL. |
| mie-opensource-landing/docs/admins/installation.md | Updates installation guidance to configure oauth2-proxy URL instead of manager URL. |
| mie-opensource-landing/docs/admins/core-concepts/external-domains.md | Replaces prior /verify semantics with oauth2-proxy topology/flags/header contract. |
| mie-opensource-landing/docs/admins/core-concepts/containers.md | Updates container docs to reflect new identity header names and oauth2-proxy auth. |
| error-pages/auth-unavailable.html | Updates copy to reference oauth2-proxy when auth is required but not configured. |
| create-a-container/views/nginx-conf.ejs | Implements oauth2-proxy auth_request flow and forwards stable identity headers to backends. |
| create-a-container/server.js | Removes verify router mount and scopes session cookies to the manager host. |
| create-a-container/routers/verify.js | Deletes the old manager forward-auth endpoint. |
| create-a-container/models/external-domain.js | Tightens validation and updates model comment for the new oauth2-proxy meaning of authServer. |
| create-a-container/client/src/pages/external-domains/ExternalDomainsListPage.tsx | Renames list column heading to oauth2-proxy. |
| create-a-container/client/src/pages/external-domains/ExternalDomainFormPage.tsx | Renames/clarifies the form field as “oauth2-proxy URL” with helper text/placeholder. |
Rather than expecting manager-specific semantics for the nginx auth_request flow, use oauth2-proxy as the expected forward-auth API. Administrators run and configure their own oauth2-proxy server; the manager no longer provides forward-auth itself. Template (views/nginx-conf.ejs): - Add /oauth2/ and internal /oauth2/auth locations that proxy to the configured oauth2-proxy upstream (the domain's authServer). - Use `auth_request /oauth2/auth` with `error_page 401 = @oauth2_signin`, redirecting (302) to the same-host /oauth2/sign_in path. - Capture oauth2-proxy's X-Auth-Request-* response headers but forward them to the backend under the stable X-User / X-Email / X-Groups / X-Access-Token contract. Manager forward-auth removal: - Delete routers/verify.js and its mount + SPA catch-all exception. - Scope the manager session cookie to its own host (drop the cross-subdomain cookie sharing that only existed for auth_request). The authServer field now points at the oauth2-proxy upstream (e.g. http://127.0.0.1:4180); model comment, form/list UI, and docs updated accordingly. Refs #348
The auth server (oauth2-proxy) is expected to be a routable host on the same load balancer (e.g. https://oauth2-proxy.example.com) that many apps delegate to, rather than a per-app loopback upstream. When NGINX proxies the /oauth2/auth subrequest to that host over the load balancer, it now pins `Host` to the auth host's own name (parsed from authServer via `new URL().host`) instead of `$host`. Sending `$host` (the app's hostname) would make the proxied request re-match the app's own server block and loop through auth_request indefinitely; pinning Host routes it to the oauth2-proxy server block. Other changes: - @oauth2_signin now 302s the browser to the auth host's /oauth2/sign_in?rd=<absolute app url>; X-Auth-Request-Redirect uses the absolute $scheme://$host$request_uri (multi-domain form) so one oauth2-proxy serves many app hosts. - proxy_ssl_server_name on so SNI matches the pinned Host on the HTTPS hop; drop the now-unused resolver (proxy_pass target is a literal). - Guard against a malformed authServer (unparseable URL) by falling back to the 503 "auth unavailable" page instead of emitting a broken proxy_pass. - authServer now documents/represents the public oauth2-proxy URL; updated model comment, form helper/placeholder, and docs (incl. --cookie-domain/--whitelist-domain guidance, the separate oauth2-proxy server-block requirement, and a loop-protection note). Refs #348
- Move authServer URL validation to the model (API layer): replace the permissive `isUrl: true` (which accepts scheme-less hosts like "oauth2-proxy.example.com") with a custom validator requiring an absolute http(s) URL. A malformed value is now rejected on create/ update with a 422 and never reaches the nginx template. - Drop the template-side `new URL()` parsing / authHost / authEnabled. The auth block again gates on `authRequired && authServer`. - Use `proxy_set_header Host $proxy_host;` (nginx's default, taken from the proxy_pass URL) instead of injecting a parsed host. This keeps the loop guard (Host = the oauth2-proxy host, never $host) without the template re-parsing the URL. Refs #348
oauth2-proxy's nginx auth_request integration expects X-Forwarded-Uri on the /oauth2/auth subrequest so it can reconstruct the original request URI for redirect/upstream context. Matches the upstream example config. Refs #348
Run oauth2-proxy as a standalone process on its own address (authServer, e.g. http://127.0.0.1:4180) and proxy the whole /oauth2/* subtree — plus the auth_request check at /oauth2/auth — straight to it, passing the app's own Host through. oauth2-proxy then terminates these requests directly, so it builds redirect URIs / cookies against the correct app hostname and needs no --reverse-proxy, --real-ip-from, or X-Forwarded-* headers. This removes the previous two-hop design (app -> routable oauth2-proxy vhost -> process), which let the proxy's own server block clobber X-Forwarded-Host and produced redirect_uris on the proxy's hostname. It also drops the Host-pinning loop guard and proxy_ssl_server_name, which are no longer needed. Admins who want oauth2-proxy behind the same load-balancer IP can expose its port via an L4 (stream{}) passthrough. Request scheme follows the authServer protocol; pair an http:// upstream with --force-https / --cookie-secure for HTTPS browsers. Updates the authServer validator/comment, the form helper, and the docs accordingly. Refs #348
Move the auth_request_set directives from server scope into `location /` (alongside auth_request). At server scope they were evaluated for the error_page named locations (@502/@403) too, which have no auth subrequest — breaking those internal redirects, so a signed-in user hitting a backend 502 got nginx's default page instead of @502. This matches the upstream oauth2-proxy nginx example, which keeps auth_request_set inside location /. Also document --session-store-type=redis as the fix for oversized _oauth2_proxy cookies (the default cookie store packs all tokens into the cookie and can exceed NGINX's header buffers). Refs #348
Root cause of the default 502 page on auth-enabled domains: a location-level `error_page` REPLACES (does not merge with) the server-level error_page list. Adding `error_page 401 = @oauth2_signin` inside `location /` therefore dropped the server-level `error_page 403 @403; error_page 502 @502;` for that location, so a signed-in user hitting a down backend got nginx's built-in 502 page. Re-declare error_page 403/502 inside the auth-enabled location /. Verified with a local nginx repro (auth subrequest 202 + dead backend): without the re-declaration nginx serves the default 502; with it, the custom @502 page renders. Also corrects the auth_request_set comment from the previous commit (the server-scope placement was not the cause; the error_page override was). Refs #348
Declare every error_page mapping once, at server scope, and remove all location-level error_page directives. Previously the protected location / re-declared error_page 401/403/502 (because a location-level error_page replaces, rather than merges with, the inherited list) and the no-URL location / declared error_page 503 — three separate places per server. Moving error_page 401 = @oauth2_signin and error_page 503 = @auth_unavailable to server scope keeps them working (verified with a local nginx repro: unauth -> sign-in redirect, auth + dead backend -> custom @502, no-URL -> @auth_unavailable) while leaving exactly one error_page block per server and nothing to re-declare. Refs #348
Now that NGINX sends requests directly to oauth2-proxy, the docs no longer need to contrast with the old reverse-proxy/multi-hop setup. Remove the "single hop", "no second hop to clobber headers", and "you do not need --reverse-proxy / --real-ip-from / X-Forwarded-*" language, which is only meaningful to someone who knew the previous arrangement. The sections now describe the direct setup positively. (General "nginx is the reverse proxy for containers" references are unchanged — that architecture is unchanged.) Refs #348
…ract Add a Users-section page describing how an app consumes the signed-in user's identity behind Require-auth: the stable request headers (X-User, X-Preferred-Username, X-Email, X-Groups, X-Access-Token), how to read and verify the access-token JWT, and how a static/serverless frontend can call /oauth2/userinfo. Documents X-Preferred-Username (added to the contract in b7a4c75) and clarifies that X-User is a stable opaque id, not a display name. The detailed identity-header table moves out of the External Domains admin page into this user-facing doc; admin/developer pages keep a short summary and link to it. Registered in the nav.
oauth2-proxy returns the identity — including the access-token JWT, which can be several KB — in the /oauth2/auth subrequest response headers. With nginx's default proxy buffers, a token larger than ~4-8k overflows the response-header buffer, so the auth subrequest fails (502) and the user gets a 500. Reproduced: a 2k token works, an 8k token breaks. Add headroom: - large_client_header_buffers 8 16k (http) for big inbound cookies/tokens - proxy_buffer_size 16k / proxy_buffers 8 16k on /oauth2/auth (the auth response carrying the token) and /oauth2/ (sign-in/callback Set-Cookie) Verified 2k/4k/8k tokens pass and nginx -t succeeds. For tokens that genuinely approach the buffer size, the Redis session store remains the recommended fix.
Consolidate the oauth2-proxy flags this manager's nginx integration requires into a single oauth2-proxy.cfg the admin can copy and fill in with their OIDC provider details (issuer, client id/secret, cookie secret). Covers set_xauthrequest, pass_access_token, the static://202 upstream, force_https/cookie_secure for the http-upstream case, and a Redis session store; with notes on per-app redirect_url derivation, shared-subdomain cookie/whitelist domains, and why reverse_proxy must stay off. Trims the now-duplicated flag snippets from the surrounding warnings.
Restructure the Authentication section to open with the oauth2-proxy config block (and the two setup steps), then follow with a "How it works" explanation and the option-detail admonitions. Switch the fence from ini to toml — oauth2-proxy's config is TOML, and the toml lexer highlights the quoted strings, inline comments, and arrays correctly where ini did not.
upstreams isn't required in auth_request-only mode — oauth2-proxy answers /oauth2/* directly and nginx serves the app, so there's nothing to upstream (validation allows an empty list). Remove it. Add the options used while testing this PR: code_challenge_method=S256 (PKCE, recommended) and skip_provider_button. Include insecure_oidc_allow_unverified_email commented out — it's a security relaxation that some IdPs need in dev but shouldn't be on by default.
Our nginx @oauth2_signin redirects with an absolute rd ($scheme://$host$request_uri). oauth2-proxy's redirect validator only accepts an absolute redirect whose host is in whitelist_domains, so it's required for every protected app — not just the multi-subdomain case. Add it to the required section of the example and correct the "Multiple apps" / "Sharing one sign-in" notes, which implied it was only needed alongside cookie_domains.
Add cookie_name = "__Host-oauth2_proxy" (locks the session cookie to the exact host — each subdomain signs in separately, which is the desired default) and cookie_samesite = "lax" (works with the OAuth redirect back from the IdP). Update the "Sharing one sign-in" guidance to note that sharing requires switching to the __Secure- prefix plus cookie_domains, since __Host- forbids a Domain attribute.
1f5f806 to
877aa19
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #348.
Replaces the manager's custom built-in forward-auth with oauth2-proxy as the expected API for nginx
auth_request. Administrators run and configure their own oauth2-proxy; the manager no longer ships forward-auth code.Deployment model
oauth2-proxy runs as a standalone process on its own address (e.g.
http://127.0.0.1:4180). The domain's oauth2-proxy URL (authServer) points directly at that process, and nginx proxies the whole/oauth2/*subtree (plus theauth_requestcheck) straight to it — a single hop.Hostthrough, so oauth2-proxy builds redirect URIs / cookies against the correct app hostname — no--reverse-proxy,--real-ip-from, orX-Forwarded-*plumbing required.stream {}) passthrough.What changed
Nginx template (
create-a-container/views/nginx-conf.ejs) — core changelocation /oauth2/and internallocation = /oauth2/authproxy directly toauthServer, sendingHost $host.auth_request /oauth2/auth;; on 401,@oauth2_signinissues a same-host302 /oauth2/sign_in?rd=….202status, keyed on cookie+authorization.error_pagemappings consolidated to a single server-scope block (a location-levelerror_pagereplaces, rather than merges with, the inherited list — this was breaking@502/@403on protected services).large_client_header_buffers 8 16k(http) andproxy_buffer_size 16k/proxy_buffers 8 16kon the/oauth2/*locations. Without this, a token over ~4–8k overflows the auth-response buffer and the subrequest fails (502 → 500).httpblock (inherited) instead of repeated per server; explanatory comments converted to non-rendered EJS template comments.Stable backend header contract
oauth2-proxy's
X-Auth-Request-*headers (with--set-xauthrequest) are forwarded to the backend under a stable contract so backends see the same names regardless of provider:X-User,X-Preferred-Username,X-Email,X-Groups,X-Access-Token(the last with--pass-access-token).Removed manager forward-auth code
routers/verify.js(the old/verifyendpoint) and its mount + SPA catch-all regex exception inserver.js.netimport that existed only for the manager's ownauth_request).authServerfieldNow represents the oauth2-proxy process address (e.g.
http://127.0.0.1:4180). API-layer validation requires an absolutehttp(s)URL (a malformed value is rejected with 422, never reaching the template). Updated the model comment, the External Domain form (label "oauth2-proxy URL" + helper/placeholder), the list column, and the 503 "Authentication Unavailable" page copy.Docs
/oauth2/userinfoendpoint for static/serverless frontends.external-domains.md: oauth2-proxy setup, required flags (--set-xauthrequest, optional--pass-access-token), HTTPS-scheme note (--force-https/--cookie-securefor an http upstream), large-cookie/Redis (--session-store-type=redis) guidance. The detailed identity-header table moved into the new user doc.containers.md,system-architecture.md,database-schema.md,installation.mdupdated accordingly.Design notes
--cookie-refreshadd_header Set-Cookieinsidelocation /, since a location-leveladd_headerwould drop the inherited server-level security headers on authenticated services.authRequired(per-service) andauthServer(per-domain) data model retained; no DB migration.Validation
nginx -tpasses on the rendered config (verified in an nginx container; modsecurity/http3 module directives stripped since they aren't in stock nginx). Large-token buffering reproduced and confirmed fixed (2k/4k/8k tokens pass; default buffers fail at 8k).node --checkpasses on modified JS.Reviewer notes
X-User-ID/X-Username/X-User-First-Name/X-User-Last-NametoX-User/X-Preferred-Username/X-Email/X-Groups/X-Access-Token.