Aller au contenu

Bases de la sécurité

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

Supabase "Invalid Refresh Token: Refresh Token Not Found" déconnecte vos utilisateurs. Trois causes, et comment le log Auth les distingue.

Vlad Tkachenko14 min de lecture
Un ticket déchiré en deux, la moitié gauche éclairée, la droite éteinte et qui tombe, devant une rangée de tickets entiers.

En bref

  • Supabase "Invalid Refresh Token: Refresh Token Not Found" signifie que Supabase Auth n'a aucune trace du refresh token envoyé par votre app : la session ne peut pas être renouvelée et l'utilisateur est déconnecté.
  • Trois choses effacent cette trace : une déconnexion (qui, en JavaScript, ferme tous les appareils par défaut), un token déjà utilisé qui revient une seconde fois, et une limite de session dans vos réglages Auth.
  • Une partie n'est qu'une ligne sans gravité venue d'un vieil onglet. Une autre déconnecte tous vos utilisateurs chaque jour. Le log Auth vous dit laquelle, et chaque cause a sa propre correction.

Vos utilisateurs vous écrivent pour dire qu'ils ont été déconnectés. L'un était au milieu d'un formulaire, et une autre est sûre de n'avoir touché à rien. Quand vous essayez l'app vous-même, elle fonctionne. Puis vous ouvrez vos logs et vous trouvez la même ligne, encore et encore :

AuthApiError: Invalid Refresh Token: Refresh Token Not Found

Supabase "Invalid Refresh Token: Refresh Token Not Found", c'est Supabase Auth, la partie de Supabase qui connecte les gens, qui dit n'avoir aucune trace du token que votre app a envoyé pour garder une session ouverte. Comme il n'y a plus rien à renouveler, votre app déconnecte l'utilisateur.

Voici la partie qu'on comprend facilement à l'envers : cette erreur est la dernière étape, et la cause est venue avant. Au moment où Supabase ne trouve plus le token, la session à laquelle il appartenait est déjà terminée. Elle a été fermée par une déconnexion, par un token envoyé deux fois, ou par une limite que quelqu'un a fixée. Allonger la durée de l'access token, ou couper la vérification qui met fin à une session, rend la ligne plus rare dans vos logs et laisse la cause exactement où elle était.

Que signifie "Invalid Refresh Token: Refresh Token Not Found" ?

Cela signifie que Supabase Auth a reçu un refresh token dont il n'a aucune trace, et qu'il a donc refusé de renouveler la session.

Chaque utilisateur connecté détient deux tokens. L'access token fonctionne comme un bracelet : il prouve qui il est pendant un moment, une heure si vous n'avez pas changé le réglage, et votre app le présente à chaque requête. Le refresh token est un ticket. Avant que le bracelet expire, votre app apporte le ticket au guichet, qui est Supabase Auth, et l'échange contre un nouveau bracelet et un nouveau ticket. Le guide des sessions de Supabase dit que les refresh tokens "never expire but can only be used once".

Refresh Token Not Found, c'est le guichet qui cherche un ticket et ne trouve rien. Le serveur répond avec HTTP 400 et le code refresh_token_not_found, que la liste des codes d'erreur Auth de Supabase décrit comme "Session containing the refresh token not found." Si le bracelet a expiré lui aussi, le client JavaScript de Supabase retire alors la session du navigateur et indique à votre app que l'utilisateur s'est déconnecté. C'est le moment où votre utilisateur tombe sur la page de connexion.

Aucun des deux tokens n'est une des clés API de votre projet, qui sont une autre question : lesquelles ont leur place dans votre app. Si votre message dit plutôt Invalid JWT, c'est le bracelet qui a été refusé, pas le ticket, et cette erreur a sa propre page. Si la personne ne peut plus se reconnecter ensuite et que le formulaire affiche Invalid login credentials, ce message couvre trois personnes différentes.

Pourquoi mes utilisateurs Supabase sont-ils déconnectés ?

Parce que la session derrière leur ticket a pris fin, et que le bracelet qu'ils portent encore les a gardés connectés jusqu'à son expiration. Ce délai, jusqu'à une heure avec le réglage par défaut, explique pourquoi les déconnexions semblent aléatoires : l'utilisateur est mis dehors un moment après la fin de la session.

Supabase Auth a quatre façons de dire qu'un ticket n'est pas valable, et chacune désigne une cause différente. Voici les messages exacts tirés du code du serveur Supabase Auth lui-même :

Le message dans votre logLe codeCe qui s'est passé
Invalid Refresh Token: Refresh Token Not Foundrefresh_token_not_foundLe guichet n'a aucune trace de ce ticket. Une déconnexion a fermé sa session, ou il a été échangé il y a assez longtemps pour que sa trace ait disparu.
Invalid Refresh Token: Already Usedrefresh_token_already_usedLe ticket a été échangé il y a plus de dix secondes et il revient. Supabase met alors fin à toute la session.
Invalid Refresh Token: Session Expired, parfois suivi de (Inactivity) ou (Revoked by Newer Login)session_expiredUne limite de session dans vos réglages Auth y a mis fin.
Invalid Refresh Token: No Valid Session Foundsession_not_foundLa session à laquelle appartenait le ticket n'existe plus.

Derrière ces messages se trouvent trois causes, dans la documentation de Supabase elle-même et dans les signalements sur ses issue trackers, et chacune a sa correction. Si plus personne ne peut se connecter, ou si toute l'app est blanche, c'est un autre problème, et Supabase est-il en panne, ou est-ce votre app ? est l'endroit où commencer.

Un refresh token ne sert qu'une fois

Chaque fois que votre app renouvelle une session, elle remet le ticket et le guichet déchire l'ancien. Supabase appelle cela la rotation des refresh tokens. L'ancien ticket fonctionne encore dix secondes, ce que le guide des sessions appelle le reuse interval, pour qu'un serveur et un navigateur qui renouvellent presque au même moment ne s'éjectent pas l'un l'autre. Un client qui a échangé son ticket et perdu la réponse sur une mauvaise connexion est couvert lui aussi : il récupère le ticket en cours.

En dehors de ces deux cas, un ancien ticket qui revient compte comme un signal d'alerte, parce qu'un token qui se présente deux fois ressemble à un token volé. Le guide dit que toute la session "is regarded as terminated and all refresh tokens belonging to it are marked as revoked". La réponse cette fois-là est Already Used.

Une étape de plus transforme Already Used en Not Found. Le serveur Supabase Auth efface les traces des tickets déchirés 24 heures après leur révocation. Le ticket périmé qui reçoit Already Used aujourd'hui reçoit donc Refresh Token Not Found demain, parce que le guichet n'a plus la trace à laquelle le comparer.

Un ticket à quatre moments. Dans les dix secondes, il fonctionne encore. Ensuite, il est refusé comme déjà utilisé, et une fois sa trace effacée un jour plus tard, le guichet ne le trouve plus du tout.

Cause 1 : quelqu'un s'est déconnecté, sur cet appareil ou sur un autre

Une déconnexion supprime la session côté guichet, et chaque ticket de cette session devient un ticket que le guichet ne trouve pas. Dans le client JavaScript de Supabase, signOut() sans rien entre les parenthèses ferme toutes les sessions de cet utilisateur, sur tous ses appareils. Le guide de déconnexion de Supabase le dit sans détour : avec ce réglage par défaut, les autres sessions échouent avec AuthApiError: Invalid Refresh Token: Refresh Token Not Found la prochaine fois qu'elles tentent de se renouveler, "even though no one signed out on that device."

Un utilisateur qui se déconnecte sur son téléphone est donc aussi déconnecté sur son ordinateur portable, jusqu'à une heure plus tard, quand le bracelet du portable expire et que son ticket revient inconnu. De là où il est, votre app l'a mis dehors sans raison.

Si chaque appareil doit rester connecté de son côté, l'appel de déconnexion doit fermer seulement la session d'où il part :

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

Demandez à votre builder d'utiliser cette forme partout où votre app déconnecte les gens. L'autre façon ordinaire de mettre fin à une session est un changement de mot de passe, que le guide des sessions de Supabase range parmi ce qui en termine une.

Sans scope, une seule déconnexion ferme la session sur tous les appareils. Avec scope local, elle ne ferme que celle de l'appareil que vous tenez.

Cause 2 : le nouveau ticket n'est jamais arrivé au navigateur

Si votre app génère ses pages sur un serveur, comme le fait une app Next.js, ce serveur renouvelle aussi les sessions. Il lit le ticket dans un cookie, l'échange au guichet, et doit renvoyer le nouveau ticket au navigateur dans un en-tête Set-Cookie de sa réponse. Quand ce cookie n'arrive pas, le navigateur garde l'ancien ticket, et la requête suivante présente un ticket que le serveur a déjà échangé. Dans les dix secondes, cela passe. Ensuite, c'est Already Used et la session se termine, et un jour plus tard le même ticket est Not Found.

Le guide de Supabase pour créer un client serveur nomme l'erreur qui provoque cela dans une app Next.js. Le code qui renouvelle la session, que Next.js 16 appelle le Proxy et les versions précédentes le middleware, doit renvoyer la réponse dans laquelle les nouveaux cookies ont été écrits : "An earlier response doesn't carry the refreshed cookies, so the user is signed out on the next request." Le même guide prévient que sous Next.js 15 et avant, un fichier nommé proxy.ts n'est jamais exécuté, "so sessions never refresh and users get signed out". Un assistant qui travaille avec la documentation la plus récente peut écrire exactement ce fichier dans une app plus ancienne.

Le guide de Supabase dit aussi pourquoi celle-ci est difficile à trouver : "it looks like users being signed out at random rather than an error in your code." Les signalements disent la même chose. Un développeur a écrit sur l'issue tracker de @supabase/ssr "I'm inadvertently logged out of my web application every day", et l'ingénieur de Supabase qui a répondu a pointé la façon dont le middleware transmettait sa réponse. Le signalement d'une app iOS décrit la déconnexion arrivant "2-3 days after logging in", et il a été fermé après que des mises à jour de la bibliothèque Swift de Supabase ont fait baisser les signalements.

Le serveur a renouvelé la session, et le nouveau ticket n'est jamais revenu. Le navigateur tient toujours celui qui a déjà été échangé.

Pour vérifier, ouvrez les outils de développement de votre navigateur sur une page qui a renouvelé la session et regardez la réponse dans l'onglet Network. Elle devrait porter un Set-Cookie avec le nouveau token, et la requête suivante devrait renvoyer ce token.

Cause 3 : une limite de session dans vos réglages Auth

À partir du plan Pro, Supabase peut mettre fin aux sessions exprès, depuis la page Sessions des réglages Auth de votre projet. Il existe trois limites, et le guide des sessions décrit chacune :

  • Les sessions à durée limitée se terminent après un temps fixe, quelle que soit l'activité de l'utilisateur.
  • Un délai d'inactivité met fin à une session qui n'a pas été renouvelée depuis un moment.
  • Une seule session par utilisateur ne garde que la connexion la plus récente : se connecter sur le téléphone ferme la session sur le portable.

Celles-ci le disent dans leur propre message, Session Expired, suivi de (Inactivity) ou (Revoked by Newer Login) pour la deuxième et la troisième. Le guide ajoute que les sessions terminées sont supprimées 24 heures plus tard, donc un ticket de l'une d'elles qui revient après ce délai reçoit Not Found à la place.

Si vous avez réglé l'une d'elles, les déconnexions sont le réglage qui fait son travail. Si ce n'était pas voulu, ou si un modèle ou un assistant l'a activée pour vous, la page montre quelle limite est active.

Le cas sans gravité

Un ticket d'une session déjà terminée, qui revient d'un vieil onglet ou d'un navigateur que personne n'a ouvert depuis des semaines, reçoit lui aussi Refresh Token Not Found. La personne derrière était déjà déconnectée. Son navigateur a demandé une fois, Supabase a dit non, et elle voit votre page de connexion, ce qu'elle aurait vu de toute façon.

Dans un log, cela ressemble à ceci : une ligne pour un utilisateur, souvent peu après que cet utilisateur s'est déconnecté quelque part, puis plus rien pour lui. Ces lignes-là sont l'erreur qui fait son travail. Celles à suivre sont les mêmes personnes encore et encore, ou beaucoup de personnes différentes le même jour.

Comment lire vos logs Auth pour cela

Ouvrez Logs dans votre tableau de bord Supabase, choisissez le type de log Auth, et cherchez refresh token dans les messages d'événement. Sélectionnez une ligne pour voir ses détails, dont l'ID de l'utilisateur, puis le filtre User réduit la liste à cette seule personne. La profondeur des logs dépend de votre plan, alors lisez-les tant que les plaintes sont récentes.

Lisez ensuite ce qui se répète :

Ce que vous voyez dans le log AuthCe que cela désigne
Un Refresh Token Not Found pour un utilisateur, et rien aprèsUn vieil onglet ou un navigateur qui revient. Sans gravité.
Refresh Token Not Found pour un utilisateur peu après sa déconnexion ailleursCause 1, une déconnexion qui a fermé tous les appareils
Already Used, puis Refresh Token Not Found un jour plus tard, pour les mêmes utilisateurs, encore et encoreCause 2, de nouveaux tickets qui n'arrivent pas au navigateur
Session Expired, avec une raison entre parenthèsesCause 3, une limite de session

Vous pouvez aussi demander à la base de données à quelle fréquence les gens se connectent. Chaque connexion ajoute une ligne à auth.sessions, et une déconnexion la supprime. Collez ceci dans le SQL Editor :

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;

Un utilisateur avec bien plus de sessions que d'appareils s'est connecté encore et encore sans jamais se déconnecter, ce que fait quelqu'un que votre app met dehors sans arrêt.

Ce qu'il ne faut pas changer pendant la correction

Vos règles Row Level Security, la détection de réutilisation et la durée de l'access token. Les trois ressemblent à une correction et laissent la cause en place, et assouplir la première peut ouvrir vos données à des inconnus.

Laissez vos règles Row Level Security tranquilles. Row Level Security est l'ensemble de règles qui décide quelles lignes reçoit chaque visiteur. Tant que des utilisateurs sont mis dehors, chaque écran dont la règle lit auth.uid() reste vide pour eux, parce que selon la documentation de Supabaseauth.uid() renvoie null quand la session a expiré. La correction tentante consiste à assouplir cette règle jusqu'à ce que l'écran se remplisse de nouveau, et il se remplit alors aussi pour des inconnus. Si quelqu'un l'a déjà fait, notre scan gratuit demande à chaque table que nomme votre app combien de lignes un visiteur sans connexion recevrait, lit le nombre sans récupérer une seule ligne et prend environ 20 secondes, sans compte : scannez votre app.

Laissez la détection de réutilisation activée. Supabase vous permet de la couper, dans les réglages avancés de la page Sessions, et qualifie cela de "generally not recommended". Une fois coupée, la cause 2 ne produit plus Already Used, et le nouveau ticket n'arrive toujours pas au navigateur.

Gardez l'access token à une heure. Un bracelet plus long retarde la déconnexion et laisse sa cause en place. Le guide de Supabase dit qu'une valeur au-delà d'une heure est "generally discouraged for security reasons".

Garder les connexions stables après la correction

Deux choses peuvent encore mal tourner une fois les déconnexions arrêtées : un changement ultérieur les fait revenir, et une règle assouplie en route laisse une table ouverte à des inconnus.

Reeve Monitor continue de vérifier depuis l'extérieur :

  • si votre app est en ligne, vérifié chaque minute, pour qu'une vague de messages "j'ai été déconnecté" ne soit pas prise pour une panne
  • la vérification de sécurité complète toutes les heures, sur jusqu'à trois apps
  • un message quand un résultat change, pour qu'une règle assouplie pendant une correction n'attende pas que vous regardiez
  • un rapport mensuel, en langage clair, de ce qu'il a vu

Reeve Care garde une copie de votre base de données Supabase :

  • une copie chiffrée selon un calendrier, sur le stockage propre de Reeve dans l'UE, en dehors de votre compte Supabase
  • le schéma auth où vivent les comptes de vos utilisateurs, pour qu'ils reviennent avec tout le reste
  • chaque copie vérifiée avant de compter, en comptant les lignes de chaque table
  • vos fichiers envoyés aussi, dès que vous connectez un accès Storage
  • une restauration en un clic, avec une copie de sécurité de l'état actuel prise d'abord
  • tout ce que fait Monitor

Que faire maintenant

Que faire

  • Cherchez refresh token dans vos logs Auth et lisez quel message se répète, et pour qui.
  • Si une déconnexion sur un appareil ferme les autres, passez à signOut({ scope: 'local' }).
  • Si votre app génère ses pages sur un serveur, assurez-vous que la réponse qui a renouvelé une session est bien celle qui atteint le navigateur, et cherchez son Set-Cookie dans l'onglet Network.
  • Regardez sur la page Sessions de vos réglages Auth s'il y a une durée limitée, un délai d'inactivité ou une seule session par utilisateur que personne n'a voulu activer.
  • Laissez Row Level Security, la détection de réutilisation et la durée de l'access token comme elles sont pendant la correction.
  • Traitez un Not Found par utilisateur comme du bruit, et les mêmes utilisateurs chaque jour comme un bug.

Commencez par le log Auth, parce que le message qui se répète vous dit laquelle des trois causes vous avez. Ensuite, une fois que les gens restent connectés, scannez votre app pour vérifier que rien n'a été assoupli en route.

FAQ

Pourquoi mes utilisateurs Supabase sont-ils déconnectés au hasard ?

Parce que la session derrière leur refresh token a pris fin, et que l'access token qu'ils détiennent encore les garde connectés jusqu'à une heure : la déconnexion arrive donc après sa cause. Les raisons habituelles sont une déconnexion sur un autre appareil, un token renouvelé qui n'est jamais arrivé au navigateur, et une limite de session dans vos réglages Auth. Le log Auth montre laquelle.

Qu'est-ce qu'un refresh token ?

C'est la moitié durable d'une session Supabase. L'access token prouve qui est l'utilisateur pendant une heure par défaut, et avant qu'il expire, votre app échange le refresh token contre une nouvelle paire. Chaque refresh token fonctionne une fois, et l'ancien cesse de fonctionner dix secondes après l'échange.

"Refresh Token Not Found" est-il le signe d'une attaque ?

À lui seul, non. Il signifie que Supabase Auth a refusé un token dont il n'a aucune trace, ce qui est la bonne réponse. La protection contre un token volé est la détection de réutilisation, qui met fin à une session quand un ancien token revient, et Supabase recommande de la laisser activée.

Pourquoi cela n'arrive-t-il qu'en production ?

Parce que la plupart des causes demandent du temps ou un second appareil. Un token périmé est refusé comme déjà utilisé le premier jour, puis comme introuvable une fois sa trace effacée un jour plus tard, et une session ouverte pendant que vous construisez dure rarement aussi longtemps. Les vrais utilisateurs se connectent aussi sur plusieurs appareils, et c'est là qu'apparaît une déconnexion qui ferme toutes les sessions.

Combien de temps dure une session Supabase ?

Par défaut, jusqu'à ce que l'utilisateur se déconnecte. L'access token dure une heure si vous ne changez rien, et le refresh token n'expire jamais mais ne fonctionne qu'une fois. À partir du plan Pro, vous pouvez limiter les sessions par durée totale, par inactivité ou à une par utilisateur, sur la page Sessions de vos réglages Auth.

Écrit par

Vlad Tkachenko

Fondateur de Reeve

Je passe mes journées à examiner des applications créées avec Lovable, Bolt, v0, Cursor et Replit, et la courte liste d'erreurs qui y reviennent sans cesse.

En savoir plus sur l'auteur

À lire ensuite

Tous les articles

Sauvegardez votre base Supabase avant la prochaine modification

Reeve en garde une copie chiffrée à intervalles réguliers et la restaure pour vous en cas de problème.

Voir les sauvegardes Supabase

Vérification externe automatisée, pas un audit complet. L'absence de problème n'est pas une garantie de sécurité.