Zum Inhalt springen

Sicherheitsgrundlagen

Supabase "Invalid Refresh Token: Refresh Token Not Found": 3 Ursachen

Supabase "Invalid Refresh Token: Refresh Token Not Found" meldet deine Nutzer ab. Drei Ursachen, und wie das Auth-Log sie trennt.

Vlad Tkachenko13 Min. Lesezeit
Ein Ticket, in zwei Teile gerissen, die linke Hälfte hell, die rechte blass und im Fallen, vor einer Reihe ganzer Tickets.

Kurz gesagt

  • Supabase "Invalid Refresh Token: Refresh Token Not Found" heißt: Supabase Auth kennt das Refresh Token nicht, das deine App geschickt hat. Die Sitzung lässt sich nicht verlängern, und der Nutzer wird abgemeldet.
  • Drei Dinge löschen diesen Eintrag: eine Abmeldung (die in JavaScript standardmäßig jedes Gerät beendet), ein schon benutztes Token, das ein zweites Mal kommt, und ein Sitzungslimit in deinen Auth-Einstellungen.
  • Manches davon ist eine harmlose Zeile aus einem alten Tab. Manches meldet jeden Tag alle Nutzer ab. Das Auth-Log zeigt dir, was davon, und jede Ursache hat ihre eigene Lösung.

Deine Nutzer schreiben dir, dass sie abgemeldet wurden. Einer war mitten in einem Formular, und eine andere ist sicher, dass sie nichts gedrückt hat. Wenn du die App selbst ausprobierst, läuft sie. Dann öffnest du deine Logs und findest immer wieder dieselbe Zeile:

AuthApiError: Invalid Refresh Token: Refresh Token Not Found

Supabase "Invalid Refresh Token: Refresh Token Not Found" ist Supabase Auth, der Teil von Supabase, der Leute anmeldet, und er sagt damit: Zu dem Token, das deine App geschickt hat, um eine Sitzung am Laufen zu halten, gibt es keinen Eintrag. Weil nichts mehr zu verlängern ist, meldet deine App den Nutzer ab.

Hier ist der Teil, den man leicht verkehrt herum versteht: Dieser Fehler ist der letzte Schritt, und die Ursache lag früher. Wenn Supabase das Token nicht mehr findet, ist die Sitzung, zu der es gehörte, schon vorbei. Beendet hat sie eine Abmeldung, ein Token, das zweimal kam, oder ein Limit, das jemand gesetzt hat. Ein länger gültiges Access Token oder das Abschalten der Prüfung, die eine Sitzung beendet, macht die Zeile in deinen Logs seltener und lässt die Ursache genau, wo sie war.

Was bedeutet "Invalid Refresh Token: Refresh Token Not Found"?

Es bedeutet, dass Supabase Auth ein Refresh Token bekommen hat, zu dem es keinen Eintrag hat, und die Sitzung deshalb nicht verlängert hat.

Jeder angemeldete Nutzer hat zwei Tokens. Das Access Token funktioniert wie ein Armband: Es weist ihn eine kurze Zeit lang aus, eine Stunde, solange du die Einstellung nicht geändert hast, und deine App zeigt es bei jeder Anfrage vor. Das Refresh Token ist ein Ticket. Bevor das Armband abläuft, bringt deine App das Ticket zum Schalter, und dieser Schalter ist Supabase Auth. Dort tauscht sie es gegen ein neues Armband und ein neues Ticket. Supabases Anleitung zu Sitzungen sagt, dass Refresh Tokens "never expire but can only be used once".

Refresh Token Not Found heißt, dass der Schalter ein Ticket nachschlägt und nichts findet. Der Server antwortet mit HTTP 400 und dem Code refresh_token_not_found, den Supabases Liste der Auth-Fehlercodes als "Session containing the refresh token not found." beschreibt. Ist auch das Armband abgelaufen, entfernt Supabases JavaScript-Client die Sitzung aus dem Browser und teilt deiner App mit, dass sich der Nutzer abgemeldet hat. In diesem Moment sieht dein Nutzer die Anmeldeseite.

Keins der beiden Tokens ist einer der API-Schlüssel deines Projekts, die sind eine eigene Frage: welche davon in deine App gehören. Steht in deiner Meldung stattdessen Invalid JWT, wurde das Armband abgelehnt und nicht das Ticket, und dieser Fehler hat eine eigene Seite. Kommt die Person danach nicht wieder hinein und das Anmeldeformular sagt Invalid login credentials, stecken hinter dieser Meldung drei verschiedene Fälle.

Warum werden meine Supabase-Nutzer abgemeldet?

Weil die Sitzung hinter ihrem Ticket geendet hat und das Armband, das sie noch tragen, sie angemeldet hielt, bis es ablief. Diese Verzögerung, mit der Standardeinstellung bis zu einer Stunde, lässt die Abmeldungen zufällig wirken: Der Nutzer fliegt eine Weile nach dem Ende der Sitzung raus.

Supabase Auth hat vier Arten zu sagen, dass ein Ticket nicht gilt, und jede zeigt auf eine andere Ursache. Das sind die genauen Meldungen aus dem Code des Supabase-Auth-Servers:

Die Meldung in deinem LogDer CodeWas passiert ist
Invalid Refresh Token: Refresh Token Not Foundrefresh_token_not_foundDer Schalter hat keinen Eintrag zu diesem Ticket. Eine Abmeldung hat seine Sitzung beendet, oder es wurde vor so langer Zeit getauscht, dass sein Eintrag weg ist.
Invalid Refresh Token: Already Usedrefresh_token_already_usedDas Ticket wurde vor mehr als zehn Sekunden getauscht und kommt wieder. Supabase beendet dann die ganze Sitzung.
Invalid Refresh Token: Session Expired, manchmal mit (Inactivity) oder (Revoked by Newer Login) dahintersession_expiredEin Sitzungslimit in deinen Auth-Einstellungen hat sie beendet.
Invalid Refresh Token: No Valid Session Foundsession_not_foundDie Sitzung, zu der das Ticket gehörte, gibt es nicht mehr.

Hinter diesen Meldungen stehen drei Ursachen, in Supabases eigener Dokumentation und in den Berichten auf seinen Issue-Trackern, und jede hat ihre eigene Lösung. Kann sich niemand mehr anmelden oder ist die ganze App leer, hast du ein anderes Problem, und ist Supabase down, oder ist es deine App ist der richtige Anfang.

Ein Refresh Token funktioniert einmal

Jedes Mal, wenn deine App eine Sitzung verlängert, gibt sie das Ticket ab, und der Schalter zerreißt das alte. Supabase nennt das Refresh Token Rotation. Das alte Ticket funktioniert noch zehn Sekunden weiter, was die Anleitung zu Sitzungen das Reuse Interval nennt, damit sich ein Server und ein Browser, die fast im selben Moment verlängern, nicht gegenseitig rauswerfen. Auch ein Client, der sein Ticket getauscht und die Antwort über eine schlechte Verbindung verloren hat, ist abgedeckt: Er bekommt das aktuelle Ticket zurück.

Außerhalb dieser beiden Fälle gilt ein altes Ticket, das zurückkommt, als Warnzeichen, denn ein Token, das zweimal auftaucht, sieht aus wie ein gestohlenes. Die Anleitung sagt, dass die ganze Sitzung dann "is regarded as terminated and all refresh tokens belonging to it are marked as revoked". Die Antwort lautet diesmal Already Used.

Ein weiterer Schritt macht aus Already Used ein Not Found. Der Supabase-Auth-Server löscht die Einträge zerrissener Tickets 24 Stunden, nachdem sie widerrufen wurden. Das veraltete Ticket, das heute Already Used bekommt, bekommt also morgen Refresh Token Not Found, weil der Schalter keinen Eintrag mehr hat, mit dem er es vergleichen könnte.

Ein Ticket zu vier Zeitpunkten. Innerhalb von zehn Sekunden funktioniert es noch. Danach wird es als schon benutzt abgelehnt, und sobald sein Eintrag einen Tag später gelöscht ist, findet der Schalter es gar nicht mehr.

Ursache 1: Jemand hat sich abgemeldet, auf diesem Gerät oder einem anderen

Eine Abmeldung löscht die Sitzung auf der Seite des Schalters, und jedes Ticket daraus wird eins, das der Schalter nicht findet. In Supabases JavaScript-Client beendet signOut() ohne etwas in den Klammern jede Sitzung dieses Nutzers, auf jedem Gerät. Supabases Anleitung zum Abmelden sagt das ausdrücklich: Mit dieser Voreinstellung scheitern die anderen Sitzungen mit AuthApiError: Invalid Refresh Token: Refresh Token Not Found, sobald sie das nächste Mal verlängern wollen, "even though no one signed out on that device."

Ein Nutzer, der sich auf dem Handy abmeldet, ist also auch auf dem Laptop abgemeldet, bis zu einer Stunde später, wenn das Armband des Laptops abläuft und sein Ticket als unbekannt zurückkommt. Von seinem Platz aus sieht es so aus, als hätte deine App ihn ohne Grund rausgeworfen.

Soll jedes Gerät für sich angemeldet bleiben, darf der Abmeldeaufruf nur die Sitzung beenden, aus der er kommt:

await supabase.auth.signOut({ scope: 'local' })

Bitte deinen Builder, diese Form überall zu verwenden, wo deine App Leute abmeldet. Der andere gewöhnliche Weg, eine Sitzung zu beenden, ist eine Passwortänderung, die Supabases Anleitung zu Sitzungen zu den Dingen zählt, die eine Sitzung beenden.

Ohne scope beendet eine Abmeldung die Sitzung auf jedem Gerät. Mit scope local beendet sie nur die auf dem Gerät in deiner Hand.

Ursache 2: Das neue Ticket ist nie im Browser angekommen

Rendert deine App Seiten auf einem Server, wie es eine Next.js-App tut, dann verlängert auch dieser Server Sitzungen. Er liest das Ticket aus einem Cookie, tauscht es am Schalter und muss das neue Ticket in einem Set-Cookie-Header seiner Antwort an den Browser zurückschicken. Kommt dieses Cookie nicht an, behält der Browser das alte Ticket, und die nächste Anfrage zeigt ein Ticket vor, das der Server schon getauscht hat. Innerhalb von zehn Sekunden geht das durch. Danach heißt es Already Used und die Sitzung endet, und einen Tag später ist dasselbe Ticket Not Found.

Supabases Anleitung zum Erstellen eines Server-Clients nennt den Fehler, der das in einer Next.js-App auslöst. Der Code, der die Sitzung verlängert und den Next.js 16 Proxy nennt und frühere Versionen Middleware, muss die Antwort zurückgeben, in die die neuen Cookies geschrieben wurden: "An earlier response doesn't carry the refreshed cookies, so the user is signed out on the next request." Dieselbe Anleitung warnt, dass unter Next.js 15 und früher eine Datei namens proxy.ts gar nicht ausgeführt wird, "so sessions never refresh and users get signed out". Ein Assistent, der mit der neuesten Dokumentation arbeitet, kann genau diese Datei in eine ältere App schreiben.

Supabases Anleitung sagt auch, warum sich dieser Fall schwer finden lässt: "it looks like users being signed out at random rather than an error in your code." Die Berichte klingen genauso. Ein Entwickler schrieb auf dem Issue-Tracker von @supabase/ssr "I'm inadvertently logged out of my web application every day", und der Supabase-Entwickler, der antwortete, verwies darauf, wie die Middleware ihre Antwort weitergab. Der Bericht zu einer iOS-App beschreibt die Abmeldung "2-3 days after logging in" und wurde geschlossen, nachdem Updates an Supabases Swift-Bibliothek die Meldungen seltener gemacht hatten.

Der Server hat die Sitzung verlängert, und das neue Ticket ist nie zurückgekommen. Der Browser hält noch das, das schon getauscht wurde.

Zum Prüfen öffnest du die Entwicklertools deines Browsers auf einer Seite, die die Sitzung verlängert hat, und siehst dir die Antwort im Tab Network an. Sie sollte ein Set-Cookie mit dem neuen Token tragen, und die nächste Anfrage sollte genau dieses Token zurückschicken.

Ursache 3: ein Sitzungslimit in deinen Auth-Einstellungen

Ab dem Pro-Plan kann Supabase Sitzungen absichtlich beenden, über die Seite Sessions in den Auth-Einstellungen deines Projekts. Es gibt drei Limits, und die Anleitung zu Sitzungen beschreibt jedes:

  • Zeitlich begrenzte Sitzungen enden nach einer festen Zeit, egal wie aktiv der Nutzer ist.
  • Ein Inaktivitäts-Timeout beendet eine Sitzung, die eine Weile nicht verlängert wurde.
  • Eine Sitzung pro Nutzer behält nur die neueste Anmeldung, also beendet eine Anmeldung auf dem Handy die Sitzung auf dem Laptop.

Diese sagen es in ihrer eigenen Meldung, Session Expired, beim zweiten und dritten mit (Inactivity) oder (Revoked by Newer Login) dahinter. Die Anleitung ergänzt, dass beendete Sitzungen 24 Stunden später gelöscht werden. Ein Ticket aus einer solchen Sitzung, das danach zurückkommt, bekommt also stattdessen Not Found.

Hast du eins davon eingestellt, sind die Abmeldungen die Einstellung bei der Arbeit. War es nicht so gemeint, oder hat eine Vorlage oder ein Assistent es für dich eingestellt, zeigt die Seite, welches Limit an ist.

Der Fall, der harmlos ist

Ein Ticket aus einer schon beendeten Sitzung, das aus einem alten Tab kommt oder aus einem Browser, den wochenlang niemand geöffnet hat, bekommt auch Refresh Token Not Found. Die Person dahinter war schon abgemeldet. Ihr Browser hat einmal gefragt, Supabase hat nein gesagt, und sie sieht deine Anmeldeseite, die sie ohnehin gesehen hätte.

Im Log sieht das so aus: eine Zeile für einen Nutzer, oft kurz nachdem sich dieser Nutzer irgendwo abgemeldet hat, und danach nichts mehr von ihm. Solche Zeilen sind der Fehler, der seine Arbeit macht. Verfolgen solltest du dieselben Leute immer wieder, oder viele verschiedene Leute am selben Tag.

So liest du deine Auth-Logs dafür

Öffne Logs in deinem Supabase-Dashboard, wähle den Log-Typ Auth und durchsuche die Event-Meldungen nach refresh token. Wähl eine Zeile aus, um ihre Details zu sehen, darunter die ID des Nutzers, und der Filter User grenzt die Liste dann auf diese eine Person ein. Wie weit die Logs zurückreichen, hängt von deinem Plan ab, also lies sie, solange die Beschwerden frisch sind.

Dann lies, was sich wiederholt:

Was du im Auth-Log siehstWorauf es hinweist
Ein Refresh Token Not Found für einen Nutzer, und danach nichtsEin alter Tab oder ein zurückkehrender Browser. Harmlos.
Refresh Token Not Found für einen Nutzer kurz nach seiner Abmeldung woandersUrsache 1, eine Abmeldung, die jedes Gerät beendet hat
Already Used, einen Tag später Refresh Token Not Found, für dieselben Nutzer, immer wiederUrsache 2, neue Tickets, die nicht im Browser ankommen
Session Expired, mit einem Grund in KlammernUrsache 3, ein Sitzungslimit

Du kannst auch die Datenbank fragen, wie oft sich Leute anmelden. Jede Anmeldung legt eine Zeile in auth.sessions an, und eine Abmeldung löscht sie. Füg das in den SQL Editor ein:

select user_id, count(*) as sessions, max(created_at) as latest
from auth.sessions
where created_at > now() - interval '14 days'
group by user_id
order by sessions desc
limit 20;

Ein Nutzer mit viel mehr Sitzungen, als er Geräte hat, hat sich immer wieder angemeldet, ohne sich je abzumelden, und genau das tut jemand, den deine App ständig rauswirft.

Was du beim Beheben nicht ändern solltest

Deine Regeln für Row Level Security, die Reuse Detection und die Gültigkeit des Access Tokens. Alle drei sehen aus wie eine Lösung und lassen die Ursache, wo sie ist, und die erste zu lockern kann deine Daten für Fremde öffnen.

Lass deine Regeln für Row Level Security in Ruhe. Row Level Security sind die Regeln, die entscheiden, welche Zeilen jeder Besucher bekommt. Solange Nutzer rausfliegen, bleibt jeder Bildschirm, dessen Regel auth.uid() liest, für sie leer, denn laut Supabases Dokumentation gibt auth.uid() null zurück, wenn die Sitzung abgelaufen ist. Die naheliegende Lösung ist, diese Regel zu lockern, bis sich der Bildschirm wieder füllt, und dann füllt er sich auch für Fremde. Hat das schon jemand getan, fragt unser kostenloser Scan jede Tabelle, die deine App nennt, wie viele Zeilen ein Besucher ohne Anmeldung bekäme, liest die Zahl, ohne eine einzige Zeile abzurufen, und braucht ungefähr 20 Sekunden ohne Konto: scanne deine App.

Lass die Reuse Detection eingeschaltet. Supabase lässt dich sie abschalten, in den erweiterten Einstellungen auf der Seite Sessions, und nennt das "generally not recommended". Ist sie aus, liefert Ursache 2 kein Already Used mehr, und das neue Ticket kommt trotzdem nie im Browser an.

Lass das Access Token bei einer Stunde. Ein längeres Armband schiebt die Abmeldung auf und lässt ihre Ursache, wo sie ist. Supabases Anleitung sagt, dass ein Wert über einer Stunde "generally discouraged for security reasons" ist.

Damit Anmeldungen nach der Lösung halten

Zwei Dinge können noch schiefgehen, wenn die Abmeldungen aufhören: Eine spätere Änderung bringt sie zurück, und eine unterwegs gelockerte Regel lässt eine Tabelle für Fremde offen.

Reeve Monitor prüft weiter von außen:

  • ob deine App erreichbar ist, jede Minute geprüft, damit eine Welle von "Ich wurde abgemeldet"-Nachrichten nicht für einen Ausfall gehalten wird
  • die vollständige Sicherheitsprüfung jede Stunde, für bis zu drei Apps
  • eine Nachricht, wenn sich ein Ergebnis ändert, damit eine beim Beheben gelockerte Regel nicht wartet, bis du hinschaust
  • ein monatlicher Bericht in einfacher Sprache über das, was er gesehen hat

Reeve Care hält eine Kopie deiner Supabase-Datenbank:

  • eine verschlüsselte Kopie nach Zeitplan, auf Reeves eigenem Speicher in der EU, außerhalb deines Supabase-Kontos
  • das Auth-Schema, in dem die Konten deiner Nutzer liegen, damit sie mit allem anderen zurückkommen
  • jede Kopie geprüft, bevor sie zählt, durch Zählen der Zeilen in jeder Tabelle
  • auch deine hochgeladenen Dateien, sobald du einen Storage-Zugang verbindest
  • eine Wiederherstellung per Klick, mit einer Sicherheitskopie des aktuellen Stands vorab
  • alles, was Monitor tut

Was du jetzt tun solltest

Was zu tun ist

  • Durchsuch deine Auth-Logs nach refresh token und lies, welche Meldung sich wiederholt, und bei wem.
  • Beendet eine Abmeldung auf einem Gerät die anderen, ändere sie auf signOut({ scope: 'local' }).
  • Rendert deine App auf einem Server, sorg dafür, dass die Antwort, die eine Sitzung verlängert hat, auch die ist, die beim Browser ankommt, und prüf ihr Set-Cookie im Tab Network.
  • Sieh auf der Seite Sessions deiner Auth-Einstellungen nach einer Zeitbegrenzung, einem Inaktivitäts-Timeout oder einer Sitzung pro Nutzer, die niemand einschalten wollte.
  • Lass Row Level Security, die Reuse Detection und die Gültigkeit des Access Tokens, wie sie sind, solange du das behebst.
  • Behandle ein Not Found pro Nutzer als Rauschen und dieselben Nutzer jeden Tag als Fehler.

Fang mit dem Auth-Log an, denn die Meldung, die sich wiederholt, sagt dir, welche der drei Ursachen du hast. Dann, sobald die Leute angemeldet bleiben, scanne deine App, um zu prüfen, dass unterwegs nichts gelockert wurde.

FAQ

Warum werden meine Supabase-Nutzer scheinbar zufällig abgemeldet?

Weil die Sitzung hinter ihrem Refresh Token geendet hat und das Access Token, das sie noch haben, sie bis zu einer Stunde lang angemeldet hält. Die Abmeldung kommt also später als ihre Ursache. Die üblichen Gründe sind eine Abmeldung auf einem anderen Gerät, ein erneuertes Token, das nie im Browser angekommen ist, und ein Sitzungslimit in deinen Auth-Einstellungen. Das Auth-Log zeigt, welcher es ist.

Was ist ein Refresh Token?

Es ist die langlebige Hälfte einer Supabase-Sitzung. Das Access Token weist den Nutzer standardmäßig eine Stunde lang aus, und bevor es abläuft, tauscht deine App das Refresh Token gegen ein neues Paar. Jedes Refresh Token funktioniert einmal, und das alte hört zehn Sekunden nach dem Tausch auf zu funktionieren.

Ist "Refresh Token Not Found" ein Zeichen für einen Angriff?

Für sich genommen nicht. Es heißt, dass Supabase Auth ein Token abgelehnt hat, von dem es keinen Eintrag hat, und das ist die richtige Antwort. Gegen ein gestohlenes Token schützt die Reuse Detection, die eine Sitzung beendet, wenn ein altes Token zurückkommt, und Supabase empfiehlt, sie eingeschaltet zu lassen.

Warum passiert das nur in Produktion?

Weil die meisten Ursachen Zeit oder ein zweites Gerät brauchen. Ein veraltetes Token wird am ersten Tag als schon benutzt abgelehnt und als nicht gefunden, sobald sein Eintrag einen Tag später gelöscht ist, und eine Sitzung, mit der du beim Bauen angemeldet bist, lebt selten so lange. Echte Nutzer melden sich außerdem auf mehreren Geräten an, und dort zeigt sich eine Abmeldung, die jede Sitzung beendet.

Wie lange hält eine Supabase-Sitzung?

Standardmäßig, bis sich der Nutzer abmeldet. Das Access Token hält eine Stunde, wenn du es nicht änderst, und das Refresh Token läuft nie ab, funktioniert aber nur einmal. Ab dem Pro-Plan kannst du Sitzungen nach Gesamtdauer, nach Inaktivität oder auf eine pro Nutzer begrenzen, auf der Seite Sessions deiner Auth-Einstellungen.

Geschrieben von

Vlad Tkachenko

Gründer, Reeve

Ich schaue mir den ganzen Tag Apps an, die mit Lovable, Bolt, v0, Cursor und Replit gebaut wurden, und die kurze Liste von Fehlern, die darin immer wieder auftauchen.

Mehr über den Autor

Weiterlesen

Alle Artikel

Sichere deine Supabase-Datenbank vor der nächsten Änderung

Reeve sichert sie verschlüsselt nach Zeitplan und stellt sie für dich wieder her, wenn etwas schiefgeht.

Supabase-Backups ansehen

Automatisierte externe Prüfung, kein vollständiges Audit. Das Fehlen von Funden ist keine Garantie für Sicherheit.