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.

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 log | Le code | Ce qui s'est passé |
|---|---|---|
Invalid Refresh Token: Refresh Token Not Found | refresh_token_not_found | Le 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 Used | refresh_token_already_used | Le 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_expired | Une limite de session dans vos réglages Auth y a mis fin. |
Invalid Refresh Token: No Valid Session Found | session_not_found | La 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.
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.
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.
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 Auth | Ce que cela désigne |
|---|---|
Un Refresh Token Not Found pour un utilisateur, et rien après | Un vieil onglet ou un navigateur qui revient. Sans gravité. |
Refresh Token Not Found pour un utilisateur peu après sa déconnexion ailleurs | Cause 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 encore | Cause 2, de nouveaux tickets qui n'arrivent pas au navigateur |
Session Expired, avec une raison entre parenthèses | Cause 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 tokendans 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-Cookiedans 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 Foundpar 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.