Skip to content

fix(web): fail closed when NEXTAUTH_SECRET is missing in production - #1229

Merged
groupthinking merged 2 commits into
mainfrom
fix-auth-gate-fail-open
Aug 7, 2026
Merged

fix(web): fail closed when NEXTAUTH_SECRET is missing in production#1229
groupthinking merged 2 commits into
mainfrom
fix-auth-gate-fail-open

Conversation

@groupthinking

Copy link
Copy Markdown
Owner

Closes #1058

The defect

apps/web/src/proxy.ts gated the entire login check on a boolean that fails open:

const AUTH_SECRET = process.env.NEXTAUTH_SECRET;
const AUTH_ENABLED = !!AUTH_SECRET;   // <-- fail OPEN
...
if (AUTH_ENABLED && request.method !== 'OPTIONS' && needsAuthentication(pathname)) { ... }

If NEXTAUTH_SECRET is absent in production, the gate is skipped entirely and:

  • /dashboard and all nested routes are served unauthenticated
  • every non-public /api/* route is served unauthenticated

One missing or unpropagated env var silently converts a login-gated app into a fully public one — with no error and no signal. The "activate-when-configured / safe rollout" intent is reasonable for local dev; it is not acceptable as implicit production behaviour.

The fix

Secure by default; insecure only by explicit, auditable declaration. New pure policy fn resolveAuthGateMode() in lib/auth-paths.ts (already the Next-free, offline-testable policy home):

secret NODE_ENV opt-out flag mode result
present any any enforce session required (unchanged)
missing production unset misconfigured fail closed — 503
missing production set disabled public, deliberately
missing dev/test disabled unchanged local DX

Design notes

  • Why 503, not 401 or a redirect? Without the secret a sign-in also cannot succeed, so redirecting to /login would infinite-loop. 503 is loud, correct, non-looping, and correctly signals an operator fault rather than a client one.
  • Public paths stay open when misconfigured — gating /api/auth/* too would make it impossible to ever complete the OAuth setup that fixes the state. Locked in by a test.
  • Internal service-to-service bypass is untouched — the x-eventrelay-internal check still runs first.
  • Opt-out is strict: accepts only `1|true|yes|on` (trimmed, case-insensitive). Anything else — including `''`, `0`, `false`, a typo, or an empty CI secret — still fails closed. A configured secret always wins over the flag, so the opt-out can never downgrade a working deployment.
  • Logged once at module init, not per request — this is a boot-time deployment fault; per-request logging would flood the sink.

Evidence

RED (before the fix)

The two fail-closed integration tests were written first and failed against the unmodified proxy():

× does not serve a protected page unauthenticated when NEXTAUTH_SECRET is missing in production
  -> expected '1' not to be '1'    // x-middleware-next: the request was passed straight through
× does not serve a protected API route unauthenticated when NEXTAUTH_SECRET is missing in production

The other 4 passed immediately, confirming the test harness models existing behaviour correctly (401 for API, 307 -> /login for pages, public routes open, dev unaffected).

GREEN

Test Files  2 passed (2)
     Tests  18 passed (18)

Negative control (guards are non-vacuous)

Reverting the single decisive line (`'misconfigured'` -> `'disabled'`) fails exactly the 4 tests that assert the fix, across both layers:

× fails closed in production when the secret is missing                                   (policy)
× allows a deliberate public production deployment only via an explicit flag              (policy)
× does not serve a protected page unauthenticated when NEXTAUTH_SECRET is missing ...     (integration)
× does not serve a protected API route unauthenticated when NEXTAUTH_SECRET is missing... (integration)
Tests  4 failed | 14 passed (18)

Full verification

check result
vitest run (whole apps/web) 255 passed / 45 files (baseline on main: 245 / 44 — +10 new, 0 regressions)
eslint src middleware.ts clean
tsc --noEmit clean
next build succeeds; middleware compiles

⚠️ Behavioural change

This is intentionally not backwards compatible for one specific case: a deployment running with NODE_ENV=production and no NEXTAUTH_SECRET today serves everything publicly; after this change it returns 503.

That is the vulnerability being fixed. If a deployment is intentionally public, set:

AUTH_ALLOW_UNAUTHENTICATED=1

Documented in .env.example, apps/web/.env.example, LAUNCH_CHECKLIST.md, and the middleware.ts docstring (which previously carried the now-stale claim "session required when NEXTAUTH_SECRET is set").

Note for reviewers

While verifying, I found a pre-existing, unrelated flaky test: src/app/api/__tests__/billing-chat-gating.test.ts times out intermittently on any machine where AI_GATEWAY_API_KEY is set in the shell, because app/api/chat/route.ts:140 then makes a real network call to the Vercel AI Gateway. vitest.config.ts neutralizes BACKEND_URL and BILLING_COOKIE_SECRET "so tests are deterministic regardless of the developer's shell env" but misses the gateway keys.

Proof it is unrelated to this PR: with these changes fully stashed, the test still fails 1-in-6 runs; with AI_GATEWAY_API_KEY unset it passes 6/6. Filed separately rather than mixed into this security fix.

`proxy.ts` gated the entire login check on `AUTH_ENABLED = !!AUTH_SECRET`,
which fails OPEN. A single missing/unpropagated `NEXTAUTH_SECRET` in
production silently served `/dashboard` and every non-public `/api/*` route
to anonymous visitors, with no error and no signal.

Replace the boolean with `resolveAuthGateMode()`:

| secret  | NODE_ENV   | opt-out | mode           |
|---------|------------|---------|----------------|
| present | any        | any     | enforce        |
| missing | production | unset   | misconfigured  |
| missing | production | set     | disabled       |
| missing | dev/test   | -       | disabled       |

`misconfigured` returns 503 rather than 401/redirect: without the secret a
sign-in cannot succeed either, so redirecting to /login would loop forever.
Public paths (incl. /api/auth/*) stay reachable so an operator can still
complete OAuth setup, and the server-to-server internal-token bypass is
unchanged. The state is logged once at module init, not per request.

Local dev DX is unchanged - a missing secret outside production still just
leaves the gate off. Opting out in production now requires an explicit,
auditable `AUTH_ALLOW_UNAUTHENTICATED=1`.

Adds 10 tests (4 pure-policy, 6 integration against `proxy()`). The two
fail-closed integration tests were confirmed RED before the fix, and a
negative control (reverting `'misconfigured'` -> `'disabled'`) fails exactly
4 of them, proving the guards are non-vacuous.

Closes #1058

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-uvai Ready Ready Preview, v0 Aug 7, 2026 8:49pm

@github-actions github-actions Bot added documentation Improvements or additions to documentation javascript Pull requests that update javascript code tests labels Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • [‘architecture-gap’, ‘bug’, ‘ci-cd’, ‘ci/cd’, ‘copilot-rabbit’, ‘documentation’, ‘duplicate’, ‘enhancement’, ‘frontend’, ‘github_actions’, ‘good first issue’, ‘help wanted’, ‘high-priority’, ‘invalid’, ‘javascript’, ‘ml-model’, ‘needs-triage’, ‘pipeline-critical’, ‘placeholder-code’, ‘priority:high’, ‘python’, ‘python:uv’, ‘question’, ‘styling’, ‘tests’, ‘v0’]

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 49936146-f6be-4f0c-a7e8-5a0511c3cbb9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 6c041d3.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Agent Completion Truth Gate: NOT_APPLICABLE

Evidence agrees.

Machine-readable verdict
{
  "details": {},
  "reasons": [],
  "verdict": "not_applicable"
}

Workflow evidence

@groupthinking

Copy link
Copy Markdown
Owner Author

Follow-up filed: the pre-existing flake described in Note for reviewers now has its own fix in #1230 (independent branch off main, not stacked on this PR). With it merged, billing-chat-gating.test.ts goes from 2/5 failures to 6/6 passes on a shell with AI_GATEWAY_API_KEY set.

@groupthinking
groupthinking merged commit 39a5c0b into main Aug 7, 2026
3 of 4 checks passed
@groupthinking
groupthinking deleted the fix-auth-gate-fail-open branch August 7, 2026 20:48
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

GRV-373

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation javascript Pull requests that update javascript code tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Harden Google sign-in auth flow

1 participant