Replace builtin proxy auth with oauth2-proxy#355
Draft
runleveldev wants to merge 10 commits into
Draft
Conversation
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
6fe0ce8 to
75c0db0
Compare
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
runleveldev
commented
Jun 16, 2026
- 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
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. |
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
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 server; the manager no longer ships forward-auth code.Deployment model
A single oauth2-proxy is published as a routable host on the same load balancer (e.g.
https://oauth2-proxy.example.com), and every protected service delegates to it. A domain's oauth2-proxy URL (authServer) is that public URL.app.example.com, …) sets its domain's oauth2-proxy URL tohttps://oauth2-proxy.example.comand enables Require auth.--cookie-domain=.example.com+--whitelist-domain=.example.comso one instance serves all sibling hosts.What changed
Nginx template (
create-a-container/views/nginx-conf.ejs) — core changeRewrote the per-service auth block to oauth2-proxy
auth_requestconventions:auth_request /oauth2/auth;witherror_page 401 = @oauth2_signin;.location = /oauth2/authproxies the subrequest to<authServer>/oauth2/auth.@oauth2_signinissues a 302 to<authServer>/oauth2/sign_in?rd=<absolute app url>;X-Auth-Request-Redirectuses the absolute$scheme://$host$request_uri(multi-domain form) so the user is returned to the originating app.202success status.Loop prevention (the important bit)
Because the auth subrequest travels to
oauth2-proxy.example.comback over the same load balancer, the template pinsHostto the auth host's own name (new URL(authServer).host) rather than$host. Sending$host(the app's hostname) would make the proxied request re-match the app'sserverblock and recurse throughauth_requestindefinitely; pinningHostroutes it to the oauth2-proxyserverblock (which has noauth_request).proxy_ssl_server_name onso SNI matches the pinnedHoston the HTTPS hop.Stable backend header contract
oauth2-proxy's
X-Auth-Request-*response headers (with--set-xauthrequest) are captured viaauth_request_setand forwarded to the backend under the stable contractX-User/X-Email/X-Groups/X-Access-Token, so backends keep the same header names regardless of the auth provider.Removed manager forward-auth code
create-a-container/routers/verify.js(the old/verifyendpoint) and its mount + SPA catch-all regex exception inserver.js..example.com) cookie sharing (and the now-unusednetimport) that existed only for the manager's ownauth_request. oauth2-proxy manages its own cookies now.authServerfield repurposedNow represents the public oauth2-proxy URL (e.g.
https://oauth2-proxy.example.com). 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
Updated
external-domains.md,system-architecture.md,database-schema.md,containers.md, andinstallation.md: the central-auth-on-the-same-LB topology, theHost-pinning loop guard, required oauth2-proxy flags (--reverse-proxy,--set-xauthrequest,--cookie-domain,--whitelist-domain, optional--pass-access-token), the stable header contract, and the requirement to run a separate oauth2-proxy server block with Require auth disabled.Design notes / deliberate decisions
--cookie-refreshadd_header Set-Cookieinsidelocation /. The server block sets HSTS / X-Frame-Options / etc. via server-leveladd_header, and nginx replaces (does not merge) inheritedadd_headerat thelocationlevel — adding it would silently strip those 5 security headers on authenticated services.authServeris fail-safe. If the URL can't be parsed, the service falls back to the 503 "Authentication Unavailable" page instead of emitting a brokenproxy_pass.authRequired(per-service) andauthServer(per-domain) data model is retained; no DB migration change required.Known residual foot-gun
If an admin enables Require auth on the oauth2-proxy host's own service, that host would
auth_requestto itself → loop. Documented as a warning rather than enforced in the template.Validation
Hostis correctly pinned to the auth host.node --checkpasses on the modified JS files.nginx -twas not run (nginx isn't installable in my environment). Directives were reviewed against the oauth2-proxy docs and nginxproxy_pass/auth_request/add_header-inheritance semantics. Please runnginx -ton a rendered config as a final gate.Reviewer notes
X-User-ID/X-Username/X-User-First-Name/X-User-Last-Nameto the stableX-User/X-Email/X-Groups/X-Access-Token.nginx -tand an end-to-end test against a live oauth2-proxy in the central-host topology.