Skip to content

Add opt-in reverse-proxy header auth (e.g. OpenHost SSO) - #2060

Open
CarlKho-Minerva wants to merge 2 commits into
CapSoftware:mainfrom
CarlKho-Minerva:carl/trusted-proxy-auth
Open

Add opt-in reverse-proxy header auth (e.g. OpenHost SSO)#2060
CarlKho-Minerva wants to merge 2 commits into
CapSoftware:mainfrom
CarlKho-Minerva:carl/trusted-proxy-auth

Conversation

@CarlKho-Minerva

@CarlKho-Minerva CarlKho-Minerva commented Jul 31, 2026

Copy link
Copy Markdown

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.

Greptile Summary

Adds opt-in reverse-proxy authentication for a configured existing account.

  • Registers a NextAuth credentials provider when the trusted-header and account-email settings are present.
  • Validates the proxy assertion and optional shared secret before loading the configured user.
  • Adds the corresponding server environment schema and an enablement warning.

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

Filename Overview
packages/database/auth/auth-options.ts Adds the conditional trusted-proxy credentials provider; its returned user remains compatible with the existing JWT, session-version, and session callbacks.
packages/env/server.ts Adds optional server-side configuration for the trusted assertion header, account email, and defense-in-depth shared secret.

Reviews (1): Last reviewed commit: "Add opt-in reverse-proxy header auth (e...." | Re-trigger Greptile

Context used:

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>
Comment thread packages/env/server.ts
Comment on lines +88 to +90
TRUSTED_PROXY_AUTH_EMAIL: z
.string()
.optional()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Suggested change
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>
Comment on lines +75 to +82
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.",
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment on lines +119 to +120
...(serverEnv().TRUSTED_PROXY_AUTH_HEADER &&
serverEnv().TRUSTED_PROXY_AUTH_EMAIL

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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.

1 participant