Add opt-in reverse-proxy header auth (e.g. OpenHost SSO) - #2060
Add opt-in reverse-proxy header auth (e.g. OpenHost SSO)#2060CarlKho-Minerva wants to merge 2 commits into
Conversation
Lets self-hosters who front Cap with an authenticating reverse proxy sign in via
that proxy instead of Cap's email code. A CredentialsProvider is added to the
providers array only when TRUSTED_PROXY_AUTH_HEADER and TRUSTED_PROXY_AUTH_EMAIL
are both set; it signs in the one configured account when the trusted header
equals "true", and fails closed otherwise. Same opt-in pattern as Grafana
auth.proxy / Gitea reverse-proxy auth.
Hardening:
- Optional TRUSTED_PROXY_AUTH_SECRET, compared in constant time and length-blind
(sha256 both sides, then timingSafeEqual), so a request reaching Cap outside the
proxy can't spoof the header.
- A boot-time console.warn whenever the provider is enabled.
Works with Cap's existing jwt/session callbacks unchanged — the jwt callback
already re-fetches by token.email and stamps id + sessionVersion. Typechecked
against next-auth 4.24.5 under --strict.
Trigger via signIn("trusted-proxy"); a login-page button is a small follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| TRUSTED_PROXY_AUTH_EMAIL: z | ||
| .string() | ||
| .optional() |
There was a problem hiding this comment.
Since this is expected to be an email address, adding .email() would catch a malformed value at startup (e.g. a username instead of an email) instead of letting it silently fail at runtime when the DB lookup returns no rows.
| TRUSTED_PROXY_AUTH_EMAIL: z | |
| .string() | |
| .optional() | |
| TRUSTED_PROXY_AUTH_EMAIL: z | |
| .string() | |
| .email() | |
| .optional() |
Per tembo's review: add .email() so a malformed value (e.g. a bare username) fails loudly at startup instead of silently returning no DB rows at runtime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| if ( | ||
| serverEnv().TRUSTED_PROXY_AUTH_HEADER && | ||
| serverEnv().TRUSTED_PROXY_AUTH_EMAIL | ||
| ) { | ||
| console.warn( | ||
| "[auth] trusted-proxy sign-in is ENABLED. Only run this behind a reverse proxy that strips client-sent copies of the trust header, or a login bypass is possible.", | ||
| ); | ||
| } |
There was a problem hiding this comment.
This console.warn lives inside the providers getter, but NextAuth's getServerSession builds its options with Object.assign({}, authOptions(), { providers: [] }), which invokes that getter. authOptions() is reconstructed on every getSession()/getCurrentUser() call (packages/database/auth/session.ts, packages/web-backend/src/Auth.ts), so _providers is undefined each time and the warning fires on every authenticated request rather than once at boot. A security warning that logs per-request becomes noise operators tune out. Guarding it with a module-level flag (e.g. let _trustedProxyWarned = false) checked here would make it fire once.
| ...(serverEnv().TRUSTED_PROXY_AUTH_HEADER && | ||
| serverEnv().TRUSTED_PROXY_AUTH_EMAIL |
There was a problem hiding this comment.
This opt-in condition is the security boundary for the provider. apps/web/__tests__/unit/auth-options.test.ts already tests the same enable/partially-configured pattern for Apple — a parallel case asserting trusted-proxy is registered only when both TRUSTED_PROXY_AUTH_HEADER and TRUSTED_PROXY_AUTH_EMAIL are set (and absent otherwise) would lock this in and catch a regression that always registers the provider.
Lets self-hosters who front Cap with an authenticating reverse proxy sign in via that proxy instead of Cap's email code. A CredentialsProvider is added to the providers array only when TRUSTED_PROXY_AUTH_HEADER and TRUSTED_PROXY_AUTH_EMAIL are both set; it signs in the one configured account when the trusted header equals "true", and fails closed otherwise. Same opt-in pattern as Grafana auth.proxy / Gitea reverse-proxy auth.
Hardening:
Works with Cap's existing jwt/session callbacks unchanged — the jwt callback already re-fetches by token.email and stamps id + sessionVersion. Typechecked against next-auth 4.24.5 under --strict.
Trigger via signIn("trusted-proxy"); a login-page button is a small follow-up.
Greptile Summary
Adds opt-in reverse-proxy authentication for a configured existing account.
Confidence Score: 5/5
The PR appears safe to merge, with the authentication provider failing closed unless its explicitly configured proxy assertion succeeds.
The new path is opt-in, rejects absent or mismatched assertions and secrets, requires an existing normalized user, and integrates with the existing session callbacks without changing their contracts.
Important Files Changed
Reviews (1): Last reviewed commit: "Add opt-in reverse-proxy header auth (e...." | Re-trigger Greptile
Context used:
packages/database)