Skip to content

Replace builtin proxy auth with oauth2-proxy#355

Draft
runleveldev wants to merge 10 commits into
mainfrom
348-oauth2-proxy-forward-auth
Draft

Replace builtin proxy auth with oauth2-proxy#355
runleveldev wants to merge 10 commits into
mainfrom
348-oauth2-proxy-forward-auth

Conversation

@runleveldev

@runleveldev runleveldev commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

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.

  • The admin exposes oauth2-proxy as its own external-domain HTTP service (Require auth off on it).
  • Each protected service (app.example.com, …) sets its domain's oauth2-proxy URL to https://oauth2-proxy.example.com and enables Require auth.
  • oauth2-proxy runs with --cookie-domain=.example.com + --whitelist-domain=.example.com so one instance serves all sibling hosts.

What changed

Nginx template (create-a-container/views/nginx-conf.ejs) — core change

Rewrote the per-service auth block to oauth2-proxy auth_request conventions:

  • auth_request /oauth2/auth; with error_page 401 = @oauth2_signin;.
  • Internal location = /oauth2/auth proxies the subrequest to <authServer>/oauth2/auth.
  • @oauth2_signin issues a 302 to <authServer>/oauth2/sign_in?rd=<absolute app url>; X-Auth-Request-Redirect uses the absolute $scheme://$host$request_uri (multi-domain form) so the user is returned to the originating app.
  • Auth-response caching keyed on cookie+authorization, on oauth2-proxy's 202 success status.

Loop prevention (the important bit)

Because the auth subrequest travels to oauth2-proxy.example.com back over the same load balancer, the template pins Host to 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's server block and recurse through auth_request indefinitely; pinning Host routes it to the oauth2-proxy server block (which has no auth_request). proxy_ssl_server_name on so SNI matches the pinned Host on the HTTPS hop.

Stable backend header contract

oauth2-proxy's X-Auth-Request-* response headers (with --set-xauthrequest) are captured via auth_request_set and forwarded to the backend under the stable contract X-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

  • Deleted create-a-container/routers/verify.js (the old /verify endpoint) and its mount + SPA catch-all regex exception in server.js.
  • Scoped the manager session cookie to its own host — dropped the cross-subdomain (.example.com) cookie sharing (and the now-unused net import) that existed only for the manager's own auth_request. oauth2-proxy manages its own cookies now.

authServer field repurposed

Now 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, and installation.md: the central-auth-on-the-same-LB topology, the Host-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

  • Security headers preserved. Deliberately did not add oauth2-proxy's optional --cookie-refresh add_header Set-Cookie inside location /. The server block sets HSTS / X-Frame-Options / etc. via server-level add_header, and nginx replaces (does not merge) inherited add_header at the location level — adding it would silently strip those 5 security headers on authenticated services.
  • Malformed authServer is fail-safe. If the URL can't be parsed, the service falls back to the 503 "Authentication Unavailable" page instead of emitting a broken proxy_pass.
  • The authRequired (per-service) and authServer (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_request to itself → loop. Documented as a warning rather than enforced in the template.

Validation

  • Rendered the EJS template across all branches (auth + routable host, auth + no URL → 503, auth + malformed URL → 503, no auth) — renders cleanly; the loop-guard Host is correctly pinned to the auth host.
  • node --check passes on the modified JS files.
  • nginx -t was not run (nginx isn't installable in my environment). Directives were reviewed against the oauth2-proxy docs and nginx proxy_pass / auth_request / add_header-inheritance semantics. Please run nginx -t on a rendered config as a final gate.

Reviewer notes

  • Breaking change: the manager is no longer the auth server, and backend identity headers changed from X-User-ID / X-Username / X-User-First-Name / X-User-Last-Name to the stable X-User / X-Email / X-Groups / X-Access-Token.
  • Draft pending a real nginx -t and an end-to-end test against a live oauth2-proxy in the central-host topology.

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
@runleveldev runleveldev force-pushed the 348-oauth2-proxy-forward-auth branch from 6fe0ce8 to 75c0db0 Compare June 16, 2026 14:46
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
Comment thread create-a-container/views/nginx-conf.ejs Outdated
Comment thread create-a-container/views/nginx-conf.ejs Outdated
- 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/auth subrequest and forwards identity to backends via stable headers (X-User, X-Email, X-Groups, X-Access-Token).
  • Removes the manager’s /verify forward-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 authServer as 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.

Comment thread create-a-container/views/nginx-conf.ejs Outdated
Comment thread create-a-container/models/external-domain.js
Comment thread create-a-container/views/nginx-conf.ejs
Comment thread mie-opensource-landing/docs/developers/system-architecture.md
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
@runleveldev runleveldev marked this pull request as ready for review June 16, 2026 16:09
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
@runleveldev runleveldev marked this pull request as draft June 19, 2026 14:59
@runleveldev runleveldev removed the request for review from cmyers-mieweb June 19, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace builtin proxy auth with OIDC Proxy

2 participants