Skip to content

fix(auth): never set AUTH_URL to empty, and catch the case that broke - #24

Merged
kunjhirapara merged 1 commit into
mainfrom
fix/auth-trusthost
Sep 13, 2026
Merged

kunjhirapara merged 1 commit into
mainfrom
fix/auth-trusthost

Conversation

@kunjhirapara

Copy link
Copy Markdown
Owner

Production reported "auth": true while every Auth.js route returned 500. Both halves of that were mine.

The empty string

AUTH_URL: ${AUTH_URL:-} was the obvious way to make the variable optional in compose. It sets it to the empty string — a set variable — which is strictly worse than an absent one, in two ways at once:

  1. It shadows the default the Dockerfile bakes in from NEXT_PUBLIC_APP_URL.
  2. It stops @auth/core's trust-host expression at the first term:
config.trustHost ??= !!(envObject.AUTH_URL ?? envObject.AUTH_TRUST_HOST ?? envObject.VERCEL ?? ...)

?? falls through only on null/undefined. "" stops the chain, and !!"" is false. With trustHost: false, assertConfig returns UntrustedHost for every request — rendered to the user as "There was a problem with the server configuration", which is indistinguishable from a missing secret. That is exactly where the debugging went.

It now defaults to NEXT_PUBLIC_APP_URL, so it stays overridable without ever being blank.

The check that should have caught it

getAuthReadiness said the deployment was fine. A green light that is wrong is worse than no light — it teaches people to stop looking at it. It now encodes both faults it missed:

Trusted host. It mirrors the trustHost expression including the ?? semantics, and distinguishes "AUTH_URL is set but empty" from "AUTH_URL is not set". The first is not something anyone guesses while debugging, so the message has to say it.

Half-configured OAuth providers. The previous version deliberately ignored the OAuth credentials, reasoning that a deployment with no Google app still supports credentials and magic-link sign-in. That is true only when the provider is absent entirely. An id without its secret makes Auth.js throw while building the provider list, which takes down every sign-in method — including the ones that need no provider at all. So id XOR secret is now fatal, while neither remains fine.

missing becomes problems, since these are diagnoses rather than a list of absent names. Still names and sentences only, never values — /api/health is public.

Verification

  • 269/269 tests pass (13 new, covering both failure modes and the empty-vs-absent distinction), tsc --noEmit clean, production build green.
  • docker-compose.yml parses; AUTH_URL resolves to ${AUTH_URL:-$NEXT_PUBLIC_APP_URL}.

Deploying

The compose change has to reach the VM — this is a docker-compose.yml fix, so a new image alone will not carry it. After redeploy, /api/auth/providers returning the provider list instead of 500 is the confirmation.

Production reported auth:true while every Auth.js route returned 500. Both
halves of that were my fault.

`AUTH_URL: ${AUTH_URL:-}` was the obvious way to make the variable optional in
compose. It sets it to the empty string, which is a set variable, and that is
strictly worse than an absent one in two ways at once: it shadows the default
the Dockerfile bakes in from NEXT_PUBLIC_APP_URL, and it stops @auth/core's

    trustHost ??= !!(AUTH_URL ?? AUTH_TRUST_HOST ?? VERCEL ?? ...)

at the first term, because `??` falls through only on null/undefined and `!!""`
is false. trustHost:false makes assertConfig return UntrustedHost for every
request, which renders as "There was a problem with the server configuration" --
indistinguishable from a missing secret, which is where the debugging went.

It now defaults to NEXT_PUBLIC_APP_URL, so it stays overridable without ever
being blank.

The readiness check said this deployment was fine. A green light that is wrong
is worse than no light, because it teaches people to stop looking at it, so it
now encodes both faults it missed:

It mirrors the trustHost expression, including the `??` semantics, and
distinguishes "AUTH_URL is set but empty" from "AUTH_URL is not set" -- the
first is not a thing anyone guesses while debugging.

It treats a half-configured OAuth provider as fatal. The previous version
ignored the OAuth credentials, reasoning that a deployment with no Google app
still supports credentials and magic-link sign-in. That is true only when the
provider is absent entirely; an id without its secret makes Auth.js throw while
building the provider list, taking down every sign-in method including the ones
that need no provider at all.

`missing` becomes `problems`, since these are diagnoses rather than a list of
absent names. Still names and sentences only, never values -- /api/health is
public.
Copilot AI lite review requested due to automatic review settings September 13, 2026 15:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@kunjhirapara
kunjhirapara merged commit 95e7f48 into main Sep 13, 2026
1 check passed
@kunjhirapara
kunjhirapara deleted the fix/auth-trusthost branch September 13, 2026 15:38
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.

2 participants