Zum Inhalt springen
tecminds

Auth.js UntrustedHost hinter Coolify/Traefik — AUTH_TRUST_HOST, AUTH_URL ohne Trailing Slash, und die callback_url, die auf localhost:3000 geblieben ist

Ein Login, der auf localhost und auf Vercel geht, stirbt auf einem Coolify-plus-Traefik-Next.js-App-Router-Stack. Support hört, der Login loope, Google sagt redirect_uri_mismatch, der Server druckt UntrustedHost: Host must be trusted, und der Redirect nach dem Sign-in landet auf localhost:3000. Auth.js v5 baut jede Auth-URL aus X-Forwarded-Host/Proto oder aus AUTH_URL — Vercel setzt trustHost für dich, Docker nicht. Zuerst die Proxy-Header fixen, dann AUTH_TRUST_HOST=true, dann eine AUTH_URL, die der öffentliche Origin ohne Trailing Slash ist. Das ist die Feldnotiz.

TTobias LüscherCo‑Founder · TecMinds2026-09-24 · 17 Min Lesezeit

Auth.js UntrustedHost hinter Coolify/Traefik — AUTH_TRUST_HOST, AUTH_URL ohne Trailing Slash, und die callback_url, die auf localhost:3000 geblieben ist

Der teuerste Login ist der, der schon eingeloggt aussah. Wir haben an einem ruhigen Nachmittag ein Auth.js-v5-Upgrade ausgeliefert — denselben Coolify-plus-Traefik-self-hosted-Next.js-App-Router-Stack, den wir für Schweizer-KMU-Produkte wie Acurio fahren — und dasselbe signIn("credentials") und signIn("google"), das auf localhost:3000 und auf einem Vercel-Preview eine Session zurückgab, ist in Production gestorben. Support hat es als "Login loopt zurück auf /login" eröffnet. Ein zweites Ticket sagte "Google zeigt redirect_uri_mismatch." Ein drittes hat einen Screenshot der Adresszeile gepastet, in der http://localhost:3000/dashboard stand — auf dem Laptop eines Kunden, in einem Zürcher Büro, ohne einen Dev-Server in Reichweite. Der Container war grün. Die Datenbank hatte den User. Das Server-Log hat gedruckt:

[auth][error] UntrustedHost: Host must be trusted. URL was: "http://nextjs:3000/api/auth/session". Read more at https://errors.authjs.dev#untrustedhost

Das ist die Aufzeichnung von Auth.js hinter Coolify/Traefik — warum UntrustedHost in Docker feuert und auf Vercel nie, was Auth.js wirklich liest, um die eigene URL zu bauen (X-Forwarded-Host / X-Forwarded-Proto, oder AUTH_URL), warum AUTH_TRUST_HOST ein Presence-Flag ist und keine URL, warum eine kopierte AUTH_URL=http://localhost:3000 jeden Callback an deinen Laptop pinnt, und warum du die Proxy-Header fixst, bevor du eine einzige Env-Variable anfasst.

Der Stack ist der, den wir letzte Woche von der Server-Actions-Seite aufgeschrieben haben. Langlebiges Coolify hält den Next.js-Container, Postgres und Traefik. Vercel ist die Kontrolle, die trustHost für dich setzt und den öffentlichen Host weiterleitet. Ein lokales next dev setzt trustHost auch für dich, aus einem anderen Grund. Coolify-plus-Traefik tut keines von beidem. Die Server-Actions-CSRF-Notiz hat den Hop, der lügt, schon benannt: Traefik, das X-Forwarded-Host mit dem Compose-Service-Namen stempelt. Dort war es Nexts CSRF-Check, der Origin mit dem forwarded Host vergleicht. Hier ist es Auth.js, das aus demselben forwarded Host URLs baut. Geschwister-Fehler. Derselbe Proxy. Andere Schicht.

Vier Tickets, ein Origin

Auth.js speichert deine öffentliche URL nirgends. Bei jedem Request auf /api/auth/* berechnet es sie, und benutzt diesen berechneten Origin dann für alles, was zählt: die redirect_uri, die es an Google schickt, die callbackUrl, die es im authjs.callback-url-Cookie ablegt, die Basis, gegen die es dein relatives redirectTo: "/dashboard" auflöst, den pages.signIn-Redirect, wenn eine Session fehlt, und die Entscheidung, ob Cookies den __Secure-- / __Host--Prefix bekommen. Ist der Origin falsch, kriegst du nicht einen Fehler. Du kriegst vier Tickets, die nichts miteinander zu tun zu haben scheinen.

UntrustedHost: Host must be trusted ist das ehrliche. Auth.js weigert sich, einen Origin aus Request-Headern zu berechnen, solange du ihm nicht gesagt hast, dass die Header vertrauenswürdig sind. In Production, in einem Container, ohne AUTH_TRUST_HOST, gibt /api/auth/session einen 500 zurück, auth() in deinem Layout gibt null, und die UI zeigt den ausgeloggten Zustand für einen User, der ein absolut gültiges Session-Cookie hat. Das ist das "Login loopt"-Ticket. Der User loggt sich ein, das Cookie wird gesetzt, der nächste Render kann es nicht lesen, Middleware schickt ihn nach /login.

redirect_uri_mismatch ist Google, das die redirect_uri liest, die Auth.js gebaut hat, und sie mit der Console vergleicht. Auth.js hat http://nextjs:3000/api/auth/callback/google oder http://localhost:3000/api/auth/callback/google gebaut. Du hast https://app.example.ch/api/auth/callback/google registriert. Google hat recht. Der Origin, den Auth.js benutzt hat, ist der Bug.

Die Adresszeile auf localhost:3000 ist der redirect-Callback, der seinen Job macht. Der Default-redirect-Callback erlaubt relative URLs und URLs auf demselben Origin wie baseUrl; alles andere fällt auf baseUrl zurück. Ist baseUrl http://localhost:3000, weil AUTH_URL das sagt, dann ist eine callbackUrl von https://app.example.ch/dashboard ein fremder Origin, und der User wird nach http://localhost:3000 geschickt — Connection refused auf einem Kunden-Laptop, oder direkt auf den eigenen Dev-Server eines Entwicklers, ausgeloggt, und es sieht exakt aus wie ein Login-Loop.

InvalidCheck — "PKCE, state or nonce OAuth check could not be performed" — ist das Cookie, das auf einem Origin gesetzt und auf einem anderen gelesen wurde. Der Sign-in hat auf https://www.app.example.ch gestartet, AUTH_URL sagte https://app.example.ch, also hat redirect_uri den Browser zurück auf den Apex geschickt, wo das authjs.pkce.code_verifier-Cookie, das der www-Host gesetzt hat, nicht existiert. Derselbe Fehler, wenn der Callback auf einer localhost-redirect_uri landet, die ein Entwickler für lokale Arbeit in der Google-Console registriert hat. Die passwordChangedAt-Notiz hat uns schon beigebracht, dass Session-Fehler Kostüme tragen. Alle vier hier tragen eines.

Was Auth.js wirklich liest

Zwei Funktionen in @auth/core entscheiden den ganzen Nachmittag. Einmal lesen, und die vier Tickets kollabieren zu einem.

Die erste setzt trustHost, wenn du es nicht getan hast:

// @auth/core, lib/utils/env.ts (setEnvDefaults) — gekürzt
config.trustHost ??= !!(
  envObject.AUTH_URL ??
  envObject.AUTH_TRUST_HOST ??
  envObject.VERCEL ??
  envObject.CF_PAGES ??
  envObject.NODE_ENV !== "production"
);

Diese ??-Kette ist die ganze "geht überall ausser auf Coolify"-Geschichte. Auf Vercel ist VERCEL gesetzt: trusted. Auf Cloudflare Pages CF_PAGES: trusted. In next dev ist NODE_ENV nicht production: trusted. In einem Docker-Container, mit next build gebaut und mit node server.js gestartet, trifft nichts davon zu, und solange du nicht AUTH_TRUST_HOST oder AUTH_URL setzt, ist trustHost false und jeder /api/auth/*-Request wirft UntrustedHost. Localhost hat nichts bewiesen. Vercel hat nichts bewiesen. Beide waren trusted aus Gründen, die es in deinem Container nicht gibt.

Die zweite baut die URL, an der Auth.js glaubt zu leben:

// @auth/core, lib/utils/env.ts (createActionURL) — gekürzt
const envUrl = envObject.AUTH_URL ?? envObject.NEXTAUTH_URL;
if (envUrl) {
  url = new URL(envUrl); // Origin gewinnt; Pathname wird als basePath behandelt
} else {
  const host = headers.get("x-forwarded-host") ?? headers.get("host");
  const proto = headers.get("x-forwarded-proto") ?? protocol ?? "https";
  url = new URL(`${proto}://${host}`);
}

Zwei Zweige, eine Regel: ist AUTH_URL gesetzt, gewinnt sie; ist sie es nicht, X-Forwarded-Host, dann Host, und X-Forwarded-Proto für das Schema. next-auth legt noch einen Schritt obendrauf — mit gesetzter AUTH_URL schreibt es den Origin des eingehenden Requests auf den Env-Origin um, bevor Auth.js ihn überhaupt sieht (reqWithEnvURL). AUTH_URL=http://localhost:3000 "hilft Auth.js" also nicht, "sich selbst zu finden." Es sagt Auth.js, dass es localhost:3000 ist, für jeden Request, von jedem Kunden, und es baut jeden Redirect auf diesem Glauben.

Das heisst, es gibt genau zwei Arten, falsch zu liegen, und sie sehen vom Browser aus identisch aus. Entweder ist AUTH_URL auf den falschen Origin gesetzt — der .env.local-Wert, der per Copy-Paste in den Coolify-Environment-Tab gewandert ist — oder AUTH_URL ist nicht gesetzt und der Proxy leitet den falschen Host oder das falsche Schema weiter. Die UntrustedHost-Meldung hat die Antwort für den zweiten Fall schon gedruckt. Lies die URL darin. http://nextjs:3000/api/auth/session ist der Request, wie der Container ihn gesehen hat: der Compose-Service-Name im Host, http, wo der Browser https getippt hat. Traefik hat sich in genau dem Fehler verraten, den du wegmachen wolltest.

AUTH_TRUST_HOST=true ist ein Presence-Flag, keine URL

Die Deployment-Docs sagen es in einem Satz: hinter einem Reverse Proxy AUTH_TRUST_HOST auf true setzen, oder trustHost: true in der Config. Das sagt Auth.js, dass der X-Forwarded-Host-Header von einem Hop geschrieben wird, den du kontrollierst.

# Coolify → deine App → Environment Variables (Runtime, nicht Build)
AUTH_SECRET=<32+ zufällige Bytes, aus `npx auth secret`>
AUTH_TRUST_HOST=true

Oder, wenn du lieber nicht davon abhängen willst, dass eine Env-Variable auf der nächsten Box, die diesen Container hostet, vorhanden ist:

import NextAuth from "next-auth";
import Google from "next-auth/providers/google";

export const { handlers, auth, signIn, signOut } = NextAuth({
  trustHost: true, // hinter Traefik / Coolify; Vercel und next dev inferieren das
  providers: [Google],
  // …
});

Schau dir die ??-Kette noch einmal an, bevor du irgendetwas anderes in dieses Feld tippst. Der Check ist Presence, nicht Wert. AUTH_TRUST_HOST=false ist ein nicht-leerer String, !!"false" ist true, und Auth.js traut dem Host. Ausschalten heisst: die Variable entfernen. Einschalten heisst: das literale true. Wir haben einen PR gesehen, der AUTH_TRUST_HOST=https://app.example.ch gesetzt hat — es hat "funktioniert", weil jeder nicht-leere Wert funktioniert, und der Reviewer hat dann eine Stunde damit verbracht, sich zu wundern, warum die URL in dieser Variable keinen Effekt auf den Redirect hat. Sie hat keinen. Das ist kein URL-Slot. Der URL-Slot ist AUTH_URL, und den brauchst du vielleicht gar nicht.

Zwei Dinge sagt dir die Kette noch. Auch das Setzen von AUTH_URL schaltet trustHost ein, weshalb Teams, die AUTH_URL=http://localhost:3000 nach Coolify kopiert haben, UntrustedHost nie gesehen haben — sie sind direkt beim Localhost-Redirect gelandet und haben den ehrlichen Fehler nie bekommen. Und trustHost ist an sich kein Security-Downgrade: es sagt "glaub dem Proxy." Die Security-Frage ist, ob der Proxy das verdient, und das ist der nächste Abschnitt, nicht dieser.

Zuerst die Proxy-Header fixen

trustHost: true heisst, Auth.js baut redirect_uri, callbackUrl und Cookie-Flags aus dem, was Traefik in X-Forwarded-Host und X-Forwarded-Proto gelegt hat. Schreibt der letzte Hop, den der Next.js-Container sieht, nextjs:3000 und http, hast du einem Lügner vertraut, und AUTH_TRUST_HOST=true macht aus UntrustedHost ein redirect_uri_mismatch — ein schlimmerer Fehler, weil jetzt Google mit drin ist. Sei ehrlich mit dem Proxy, bevor du ihm traust.

Schreib die Werte auf, bevor irgendwer den Coolify-Service neu startet. Ein Wegwerf-Route-Handler, den du nach dem Vorfall löschst:

// app/api/debug-auth-origin/route.ts — nach dem Vorfall löschen
import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";

export async function GET(request: Request) {
  const h = request.headers;
  const forwardedHost = h.get("x-forwarded-host");
  const host = h.get("host");
  const proto = h.get("x-forwarded-proto");

  return NextResponse.json({
    host,
    xForwardedHost: forwardedHost,
    xForwardedProto: proto,
    // was createActionURL tut, wenn AUTH_URL nicht gesetzt ist:
    computedOrigin: `${proto ?? "https"}://${forwardedHost ?? host}`,
    // nur Presence — nie die Werte drucken
    env: {
      AUTH_URL: Boolean(process.env.AUTH_URL),
      NEXTAUTH_URL: Boolean(process.env.NEXTAUTH_URL),
      AUTH_TRUST_HOST: Boolean(process.env.AUTH_TRUST_HOST),
      VERCEL: Boolean(process.env.VERCEL),
    },
  });
}

Du willst, dass computedOrigin https://app.example.ch liest. Unseres las http://nextjs:3000. Diese Zeile, nicht die Auth.js-Config, war der Vorfall.

Traefiks passHostHeader defaultet auf true und ist nötig und nicht hinreichend — die Server-Actions-Notiz ist schon durchgegangen, warum ein innerer Hop den Compose-Namen trotzdem in X-Forwarded-Host stempeln kann. Dieselbe Middleware fixt beide Schichten:

# Traefik-File-Provider-Skizze. Coolify-Labels sind dieselbe Middleware.
http:
  middlewares:
    public-forwarded-host:
      headers:
        customRequestHeaders:
          X-Forwarded-Host: "app.example.ch"
          X-Forwarded-Proto: "https"

Coolify kennt die öffentliche Domain, die du am Service hängen hast, schon — es ist derselbe String, den es dem Container als COOLIFY_FQDN / COOLIFY_URL gibt. Der Job ist, diesen String zum Wert von X-Forwarded-Host zu machen, nicht den Docker-DNS-Namen, mit dem Traefik Port 3000 gefunden hat. Wenn du später nach COOLIFY_URL greifst, um AUTH_URL abzuleiten: ein Service mit zwei angehängten Domains bekommt dort eine kommaseparierte Liste. Ein Origin. Such ihn aus.

X-Forwarded-Proto ist die Hälfte, die niemand prüft. Auth.js entscheidet useSecureCookies aus dem Schema der URL, die es berechnet hat. https gibt dir __Secure-authjs.session-token und __Host-authjs.csrf-token; http gibt dir authjs.session-token ohne Secure-Flag. Ein Proxy, der TLS terminiert und dann vergisst, das zu sagen, lässt Auth.js Production-Cookies ausstellen, als wärst du auf blankem HTTP. Sitzt Traefik selbst hinter Cloudflare oder einem Tunnel, überschreibt Traefik eingehende X-Forwarded-* von einer Quelle, der es nicht traut — das ist korrektes Verhalten, und dafür gibt es die EntryPoint-forwardedHeaders.trustedIPs. Dem Upstream-Hop trauen, sonst reportet Traefik http für einen Request, den der User über https gemacht hat.

X-Forwarded-Host nicht vom Client nehmen. Der Browser kann schicken, was er will, und mit trustHost: true baut Auth.js eine redirect_uri darauf. Traefik setzt den Header; der Container traut nur dem Proxy-Hop. Die URL-Token-Portal-Notiz hat das für X-Forwarded-For und IP-gekeyte Limits gesagt. Dieselbe Header-Familie, dieselbe Regel: der Hop, dem du traust, muss der sein, der den Header geschrieben hat.

Die Proxy-Config redeployen, nicht nur das Next.js-Image. Wir haben einen Coolify-Restart am Web-Service verbrannt, während Traefik die alten Labels behalten hat. Der Container kam grün hoch. computedOrigin blieb http://nextjs:3000.

Dann, und erst dann, AUTH_URL

Mit ehrlichem Proxy und AUTH_TRUST_HOST=true sind die Deployment-Docs deutlich: AUTH_URL ist "mostly unnecessary with v5 as the host is inferred from the request headers." Lass sie weg. Jede URL, die Auth.js baut, folgt dem öffentlichen Host und Schema, das Traefik weiterleitet — auch an dem Tag, an dem Marketing dich von app.example.ch nach portal.example.ch verschiebt, ohne Env-Änderung.

Setz sie, wenn eines von zwei Dingen zutrifft. Du fährst Auth.js auf einem anderen Base Path als /api/auth, dann trägt AUTH_URL den Pfad und Auth.js leitet basePath daraus ab. Oder du hast einen Hop, den du nicht ehrlich machen kannst — ein geerbter Proxy, eine zweite interne Domain, die den Container auch erreicht — und willst lieber den Origin pinnen als Headern trauen. In beiden Fällen ist der Wert der öffentliche Origin, https, und er ist exakt:

# Gut — nur Origin, kein Trailing Slash. basePath bleibt /api/auth.
AUTH_URL=https://app.example.ch

# Gut — nur wenn dein Handler wirklich auf einem eigenen Base Path liegt.
# next-auth liest den Pathname und macht ihn zum basePath.
AUTH_URL=https://app.example.ch/api/auth

# Schlecht — die .env.local, die per Copy-Paste nach Coolify gewandert ist.
# Jede redirect_uri und callbackUrl wird jetzt auf deinem Laptop gebaut.
AUTH_URL=http://localhost:3000

# Schlecht — falscher Suffix. basePath wird /auth, deine Route liegt auf /api/auth,
# Client-signIn() POSTet auf /auth/signin und kriegt 404; der Handler kann
# Actions auf seinem eigenen Pfad nicht mehr parsen (UnknownAction).
AUTH_URL=https://app.example.ch/auth

# Schlecht — der interne Name. UntrustedHost geht weg, redirect_uri_mismatch kommt.
AUTH_URL=http://nextjs:3000

Die Trailing-Slash-Geschichte ist kleiner als die Folklore und trotzdem eine Zeile wert. Auth.js strippt einen Trailing Slash vom Origin, bevor es Actions anhängt, also bricht https://app.example.ch/ allein nichts. Ein Trailing Slash nach einem Base Path ist der Punkt, an dem es nicht mehr gratis ist: next-auth nimmt den Pathname als basePath wörtlich, und jetzt ist basePath /api/auth/, wo deine Route und deine OAuth-Console /api/auth sagen. Es parst meistens trotzdem; es loggt auch env-url-basepath-mismatch, wenn die beiden uneins sind, und es ist der Grund, warum sich die redirect_uri in der Google-Console und die im Network-Tab in einem Fall, den wir nicht genossen haben, um ein Zeichen unterschieden haben. Schreib den Origin. Musst du den Pfad schreiben, schreib exakt den Pfad, auf dem die Route liegt. Nichts dahinter.

NEXTAUTH_URL ist der v4-Name, und Auth.js ehrt ihn weiter als Fallback. Sind beide gesetzt, gewinnt AUTH_URL. Bist du von v4 migriert und Coolify hält noch ein NEXTAUTH_URL=http://localhost:3000 aus einem alten Import, hast du einen Localhost-Pin, den du in der neuen Variablenliste nicht siehst, bis du danach suchst. Die alte löschen. Nicht zwei mitschleppen.

Und AUTH_URL nicht setzen, um UntrustedHost verschwinden zu lassen. Es verschwindet — AUTH_URL schaltet trustHost ein — und du hast einen ehrlichen Fehler durch einen gepinnten Origin ersetzt, der beim ersten Domain-Wechsel veraltet. AUTH_TRUST_HOST=true plus ein ehrlicher Proxy ist der Fix. AUTH_URL ist eine Entscheidung über Base Paths, kein Schmerzmittel.

Die Kostüme, die den Nachmittag klauen

Fünf andere Fehler treten auf diesem Stack als "Login ist kaputt" auf. Sie benennen, damit sie das trustHost-Ticket nicht fressen.

passwordChangedAt und Verwandte. Ein jwt-Callback, der das Token bei einem fehlenden Custom-Claim droppt, bounced einen frischen Login bei Request N+1 nach /login — die passwordChangedAt-Notiz in voller Länge. Vom Browser aus sieht es exakt aus wie UntrustedHost. Es druckt kein Host must be trusted. Das Log greppen, bevor du Env anfasst.

Middleware, die auf die interne URL redirectet. new URL("/login", request.url) löst gegen den Request auf, wie der Container ihn gesehen hat. Hat Traefik Host auf den Service-Namen umgeschrieben, geht dieser Redirect nach http://nextjs:3000/login, und der Browser kann das nicht auflösen. Der Cron-Middleware-Abschnitt hatte diesen Matcher schon dabei, einen GET zu klauen; hier klaut er das Redirect-Ziel. X-Forwarded-Host und passHostHeader fixen, und der Redirect fixt sich selbst.

Server Actions CSRF auf demselben Proxy. Ein <form action={signInAction}>, das signIn() in einer Server Action aufruft, kann vor Auth.js aborten, mit x-forwarded-host does not match origin. Das ist die Server-Actions-Notiz, nicht diese — aber es ist derselbe lügende Header. Ein ehrliches X-Forwarded-Host schliesst beides. Weitest du nur allowedOrigins auf, baut Auth.js eine Schicht tiefer weiter redirect_uri auf dem internen Namen.

Vercel grün, Coolify kaputt. Derselbe Commit geht auf einem Vercel-Preview durch, weil VERCEL gesetzt ist und die Plattform den öffentlichen Host weiterleitet. Dieser Deploy beweist den Code. Er beweist nichts über trustHost in deinem Container oder über Traefik. Es gibt auf Coolify auch keine Deployment Protection, der man die Schuld geben könnte — die SSO-Wand aus der Cron-Notiz existiert hier nicht; wenn der Callback 401t oder 500t, ist es dein Handler oder dein Proxy. Auf der öffentlichen Coolify-Domain reproduzieren, mit der Debug-Route, bevor du noch einen Forum-Thread liest.

Env, die den Prozess nicht erreicht hat. Coolify unterscheidet Build-Time- und Runtime-Variablen und lädt keine davon hot nach. Ein in der UI ergänztes AUTH_TRUST_HOST ist inert, bis der Service redeployed ist. Ein in Production fehlendes AUTH_SECRET druckt MissingSecret, nicht UntrustedHost, und killt ebenfalls jeden /api/auth/*-Call; lies, welches von beiden du hast. Eine NEXT_PUBLIC_*-Variable muss für Auth.js nicht existieren — es ist Server-seitig, und die URL, die es baut, sollte das Client-Bundle auch nicht hardcoden.

Keines davon ist ein Grund, AUTH_URL auf das zu setzen, was den Fehler wegmacht. Der Fehler ist eine Messung. Ändere das, was er gemessen hat.

Die Checkliste, die wir nach einem Login abarbeiten, der "loopt"

Fünf Checks, in dieser Reihenfolge, bevor irgendwer die Auth.js-Config editieren darf.

Den Server-String lesen, nicht den Browser. UntrustedHost: Host must be trusted ist diese Notiz — und die URL in dieser Zeile ist die Sicht des Containers auf den Request; steht dort http:// oder ein interner Host, ist der Proxy schon belastet. redirect_uri_mismatch ist der Origin, den Auth.js berechnet hat, versus die OAuth-Console. InvalidCheck ist ein State-/PKCE-Cookie, das auf einem anderen Origin gesetzt wurde, als der Callback angekommen ist. MissingSecret ist AUTH_SECRET. Ein /login-Bounce ganz ohne Auth.js-Fehler ist ein jwt-Callback oder Middleware.

Localhost und Vercel sind nicht Coolify. NODE_ENV !== "production" traut dem Host in next dev. VERCEL traut ihm auf Vercel. Dein Container hat keines von beiden. Auf der öffentlichen Coolify-Domain reproduzieren und dort /api/debug-auth-origin aufrufen.

X-Forwarded-Host und X-Forwarded-Proto auf den öffentlichen Host und https setzen. Traefik-letzter Hop. passHostHeader, ein explizites öffentliches X-Forwarded-Host, wenn ein innerer Hop es überschrieben hat, forwardedHeaders.trustedIPs, wenn Traefik hinter einem weiteren Proxy sitzt. Nur dem Proxy trauen. Zuerst den Proxy redeployen, dann die App. Wenn computedOrigin https://app.example.ch liest, weitermachen; nicht vorher.

AUTH_TRUST_HOST=true, literal, Runtime, redeployen. Oder trustHost: true in der Config. Presence zählt; false ist nicht aus. Keine URL. Dann bestätigen, dass /api/auth/session JSON statt einem 500 zurückgibt.

AUTH_URL nur, wenn du sie brauchst, und dann der öffentliche Origin, exakt. Kein localhost. Kein interner Name. Kein Trailing Slash nach einem Base Path. Kein /auth, wo die Route /api/auth ist. Jedes übrig gebliebene NEXTAUTH_URL löschen. Ist der Proxy ehrlich und der Base Path Default, weglassen und die Header den Origin tragen lassen.

Drei Regeln, die das nächste Proxy-Flag überleben

Drei Regeln überleben diese Aufzeichnung und verallgemeinern sich über das hinaus, wie Coolify nächstes Jahr eine Domain nennt.

Auth.js kennt seine URL nicht. Es berechnet sie, pro Request, aus einer von zwei Quellen. AUTH_URL, wenn gesetzt; sonst X-Forwarded-Host und X-Forwarded-Proto. Jede redirect_uri, jede callbackUrl, jeder Cookie-Prefix folgt dieser Berechnung. Ein Login, der auf localhost:3000 landet, ist kein Browser-Bug. Es ist der Origin, den du ihm gegeben hast.

trustHost ist ein Presence-Flag über den Proxy, keine URL über die App. Vercel und next dev setzen es für dich, aus Gründen, die dein Container nicht hat. AUTH_TRUST_HOST=true sagt "glaub Traefik." Mach Traefik zuerst glaubwürdig: öffentlicher Host, https, vertrauter Upstream. Dann, und erst dann, das Flag umlegen.

AUTH_URL ist eine Base-Path-Entscheidung, kein Schmerzmittel. Sie bringt auch UntrustedHost zum Schweigen, und so pinnt eine kopierte .env.local Production an einen Laptop, ohne dass jemand den ehrlichen Fehler sieht. Öffentlicher Origin, kein Trailing Slash nach einem Base Path, oder weglassen. Der Server-Actions-CSRF-Abort auf derselben Box ist das Geschwister: derselbe lügende Header, eine Schicht höher. Den Header fixen, und beide werden still.

Die Komposition ist die Notiz. Wir haben einen loopenden Login als kaputte Auth.js-Config behandelt, weil localhost grün war, Vercel grün war und der Coolify-Container grün war. Production war ein Traefik-Hop, der nextjs:3000 über http forwarded hat, ein Container ohne AUTH_TRUST_HOST, weil nichts anderes es je gebraucht hatte, und eine AUTH_URL, die noch localhost:3000 sagte, seit dem Tag, an dem das Repo geklont wurde. Ein ehrliches X-Forwarded-Host, ein literales true und eine gelöschte Env-Zeile hätten das Loch in der ersten Minute gezeigt — derselben Minute, die wir mit dem Neu-Registrieren von Redirect-URIs in der Google-Console verbracht haben.

Wenn Auth.js hinter Coolify nach /login loopt und derselbe Sign-in auf localhost geht — buche einen kostenlosen AI-Potenzial-Check. Die Server-Actions-CSRF-Aufzeichnung ist derselbe Proxy, der einen anderen Check anlügt; die Auth.js-JWT-Notiz ist die Erinnerung, dass ein Bounce nach /login ein Kostüm ist, bis der Server-String sagt, wessen.

acurio · Halluzinierte Zitate? Nicht in deinem Manuskript.

Citation‑Checker für Zotero. Findet halluzinierte oder nur teilweise gestützte Quellen in KI‑geschriebenen Texten. Thesis‑Pakete ab CHF 19, Schweizer Datenverarbeitung.

NÄCHSTER SCHRITTHat dich das interessiert?