Fix Auth.js UntrustedHost in Production
A local reproduction separates the host-trust guard from proxy configuration, environment inference and later sign-in failures.
TL;DR#
Auth.js throws UntrustedHost when its host-trust configuration is false. For a self-hosted app, first ensure your ingress accepts the intended public hostname and supplies trustworthy host and protocol headers. Then set AUTH_TRUST_HOST=true in the server runtime, or explicitly enable trustHost in your Auth.js configuration. This enables a trust decision; it does not secure a proxy or finish an OAuth login.
The original production report used SvelteKit and showed this error after a successful development setup:
[UntrustedHost]: Host must be trusted.The same guard exists in Auth.js core, which the Next.js integration uses. This article covers that guard and a Next.js deployment configuration. If your log instead contains the older next-auth missing-secret error, follow the separate production NO_SECRET fix; host trust and secret configuration are different checks.
Why it happens after the production build#
The core assertion source checks the resolved trustHost option and returns an UntrustedHost error when it is false. A correctly spelled public URL in a request does not itself satisfy that guard. Neither does adding forwarded headers while leaving the option false.
The environment-default implementation explains the development difference. Unless you explicitly configure trustHost, its inference considers AUTH_URL, AUTH_TRUST_HOST, the supported platform indicators VERCEL and CF_PAGES, then whether NODE_ENV differs from production. A development server can therefore work without the production environment setting you need on a plain container or virtual machine.
The Auth.js deployment guide documents enabling host trust behind a reverse proxy. However, a trust flag is a promise about the deployment, not a hostname filter. The core configuration contract makes the deployment responsible for setting the incoming host safely. Do not solve this by allowing clients to choose arbitrary forwarded host values.
Reproduce the guard without a real account#
The following bounded reproduction ran on @auth/core 0.41.3 with Node 24.22.2 in the official Alpine Node container on 2 November 2026. It requests only the provider-list endpoint. The local Credentials provider always rejects credentials, so no external account, OAuth server, database or successful user session is involved.
Create an empty directory and install the tested package:
npm init -y
npm install --save-exact @auth/core@0.41.3Save this as host-check.mjs, then run it with Node. The diagnostic secret below belongs only to this disposable script; generate and store a proper secret for your application.
import { Auth } from '@auth/core';
import Credentials from '@auth/core/providers/credentials';
const config = {
secret: 'local-reproduction-only-secret-never-deploy-this',
basePath: '/api/auth',
providers: [Credentials({
id: 'local',
name: 'Local test only',
authorize: async () => null,
})],
logger: {
error(error) {
console.log(`ERROR ${error.type}: ${error.message.split(' Read more at')[0]}`);
},
warn() {},
debug() {},
},
};
for (const trustHost of [false, true]) {
const response = await Auth(
new Request('https://login.example.com/api/auth/providers'),
{ ...config, trustHost },
);
console.log(`trustHost=${trustHost}: status=${response.status}`);
}The recorded harness used the labels below for those same two calls. Only the trustHost boolean changed between the before and after requests:
ERROR UntrustedHost: Host must be trusted. URL was: https://login.example.com/api/auth/providers.
before: status=500
after-one-change: status=200As a negative control, another call supplied X-Forwarded-Host and X-Forwarded-Proto while keeping trustHost false. It still failed:
forwarded-headers-only: status=500This proves the configuration guard and its minimal correction. It does not prove that a public proxy is secure, that an OAuth callback is registered correctly, or that a real user can sign in. The Next.js files below follow the framework contract; a full Next.js production deployment and an external login were not part of this reproduction.
Fix the Next.js deployment at the trust boundary#
- Establish which system controls the public hostname.
For the example deployment, the browser uses https://login.example.com and the Node process sits behind an ingress. Configure that ingress to accept the expected hostname, reject unknown hosts and overwrite client-supplied forwarded host and protocol headers. Restrict direct access to the Node service so clients cannot bypass the ingress.
For an existing Nginx virtual host that terminates HTTPS for this single domain, the proxy location can pin these values. The upstream here is a Node service on the same machine; use your actual private service address in a container network.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host login.example.com;
proxy_set_header X-Forwarded-Host login.example.com;
proxy_set_header X-Forwarded-Proto https;
}This location belongs inside your existing TLS server configuration, alongside its certificate and hostname policy. It is not a complete Nginx installation. The Nginx proxy module reference documents overriding upstream request headers. Pinning the values suits this single-domain example; a multi-domain app needs an explicit, validated domain policy instead.
- Enable host trust in the process that serves auth requests.
Once the ingress contract is true, add this to the production server environment:
AUTH_TRUST_HOST=trueKeep your existing AUTH_SECRET and provider credentials in the same server environment. Do not copy the reproduction secret. Update the running deployment by restarting or redeploying through your host's normal mechanism; changing a local file does not alter an already running remote container.
Alternatively, add the boolean trustHost option to your existing Auth.js configuration. This complete example uses GitHub's provider with environment inference; it assumes the matching provider ID and secret are already configured on the server:
// auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub],
trustHost: true,
});The environment flag and the explicit option are alternatives for expressing trust. An explicit false option wins over the environment defaults; adding the environment flag will not override it. Remove an accidental explicit false after verifying the ingress.
- Confirm the framework handler and server environment.
The Auth.js installation instructions use an App Router handler that exports the configured methods:
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;Use the import path that matches your app's root or src layout. These examples target the Auth.js-style NextAuth initialiser, rather than retrofitting the same options into an older next-auth API shape. Check the installed dependency before changing configuration syntax.
Auth variables belong on the server. Next.js environment documentation distinguishes server environment values from variables exposed to browser bundles. Do not prefix the auth secret or trust flag with NEXT_PUBLIC_. For a container deployment, supply them to the launched container, rather than assuming a file on the image-building machine is present at runtime.
AUTH_URL pins an origin; AUTH_TRUST_HOST enables trust#
AUTH_TRUST_HOST is not the place for your domain. Use the documented true value for the flag. AUTH_URL is the URL setting, and it is usually unnecessary when request inference already produces the correct public URL.
For a fixed-origin deployment whose internal request origin is wrong, the Next.js integration source uses AUTH_URL to replace the request origin. The same integration defaults the auth base path to /api/auth. With that default route layout, a fixed public origin can be configured as:
AUTH_URL=https://login.example.comThe core environment helper also treats a populated AUTH_URL as a host-trust signal. Therefore, adding AUTH_TRUST_HOST to an app already using AUTH_URL might change nothing. Inspect the resolved configuration and ingress behaviour before adding more variables.
Do not confuse /api/auth with /auth when moving examples between integrations. If you have a custom base path, align the handler location, configuration and public URL path. Setting a URL does not create the route on the server.
There is a further trap: in the tested core helper, the string false is truthy. These are recorded configuration results, not recommendations:
production-unset: trustHost=false
development: trustHost=true
trust-env-true: trustHost=true
trust-env-false-string: trustHost=true
fixed-origin: trustHost=true
explicit-false: trustHost=falseTo remove an environment-based signal, delete the variable rather than assigning the text false. Other signals can still enable trust. To deliberately test the rejection path, use the boolean false option in code.
Verify the public URLs before testing a login#
Request the provider endpoint on the deployed public domain:
curl -i https://login.example.com/api/auth/providersWith providers configured, expect a successful JSON response whose sign-in and callback URLs use your public HTTPS origin. A successful provider listing shows that this request passed the configuration guard; it is not an authentication test. Inspect the server logs at the same time, because the browser's generic configuration message can conceal a different server error.
Check the ingress separately. In a controlled deployment test, send an unexpected forwarded host and confirm it is overwritten or rejected, and verify the Node service is inaccessible directly. In the local URL-helper control, a supplied untrusted forwarded host changed an inferred action URL; a fixed AUTH_URL pinned that helper's output. Neither outcome established that the surrounding network enforced your policy. That enforcement belongs to the ingress and its access controls.
If the next failure is a CredentialsSignin result, continue with handling failed NextAuth credentials, which explains reading the sign-in result's error. If the browser instead reports CORS at Google's authorisation endpoint after a fetch redirect, use the OAuth navigation fix. For an OAuth provider rejecting its callback URL, compare the actual generated callback with the provider registration, including scheme, domain and path. Enabling host trust does not repair those separate failures; it lets a correctly configured deployment get past this particular guard.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Fix next-auth /api/auth/error in production: NO_SECRET
Reproduced on next-auth 4.24.15 and Next.js 15.5.28: why sign-in works on localhost but fails after deploy, what a 404 versus a 500 at /api/auth/error tells you, and the env change that fixes it.
Next.js Build Passed But Production Broke
A green build only proves the bundle compiled. This postmortem walks through three production-only failures: missing Vercel env vars, Edge runtime imports, and cache behavior that changed after deploy.
NextAuth login failed: handle CredentialsSignin, v4 and v5
Reproduced on next-auth 4.24.15 and 5.0.0-beta.32: what signIn() returns on a wrong password, why the accepted if (ok) check breaks on v5, and how to get a usable error code.


