Перейти до вмісту

Основи безпеки

Supabase "Invalid Refresh Token: Refresh Token Not Found": 3 причини

Supabase "Invalid Refresh Token: Refresh Token Not Found" виводить ваших користувачів із системи. Три причини і як лог Auth їх розрізняє.

Vlad Tkachenko11 хв читання
Квиток, розірваний навпіл: ліва половина світиться, права згасла й падає, а позаду ряд цілих квитків.

Коротко

  • Supabase "Invalid Refresh Token: Refresh Token Not Found" означає, що Supabase Auth не має запису про refresh token, який надіслав ваш застосунок. Сесію не можна поновити, і користувача виводить із системи.
  • Цей запис зникає з трьох причин: вихід із системи (у JavaScript він за замовчуванням завершує всі пристрої), уже використаний токен, що приходить удруге, і ліміт сесій у налаштуваннях Auth.
  • Частина цього лише нешкідливий рядок зі старої вкладки. Інша частина щодня виводить усіх ваших користувачів. Лог Auth скаже, що саме у вас, і кожна причина має своє виправлення.

Ваші користувачі пишуть, що їх викинуло із системи. Один був посеред заповнення форми, а інша певна, що нічого не натискала. Коли ви пробуєте застосунок самі, він працює. Потім ви відкриваєте логи й бачите той самий рядок, знову і знову:

AuthApiError: Invalid Refresh Token: Refresh Token Not Found

Supabase "Invalid Refresh Token: Refresh Token Not Found" означає, що Supabase Auth, частина Supabase, яка відповідає за вхід, не має запису про токен, який ваш застосунок надіслав, щоб підтримати сесію. Поновлювати більше нічого, тож застосунок виводить користувача із системи.

Ось що легко зрозуміти навпаки: ця помилка є останнім кроком, а причина була раніше. Коли Supabase уже не знаходить токен, сесія, якій він належав, уже завершилася. Її завершив вихід із системи, токен, надісланий двічі, або ліміт, який хтось встановив. Довший термін дії access token чи вимкнена перевірка, яка завершує сесію, роблять цей рядок у логах рідшим і залишають причину там, де вона була.

Що означає "Invalid Refresh Token: Refresh Token Not Found"?

Це означає, що Supabase Auth отримав refresh token, про який не має запису, і тому відмовився поновлювати сесію.

Кожен користувач у системі має два токени. Access token працює як браслет: він якийсь час підтверджує, хто це, годину, якщо ви не змінювали налаштування, і застосунок показує його з кожним запитом. Refresh token це квиток. Перш ніж браслет сплине, застосунок несе квиток до стійки, якою є Supabase Auth, і обмінює його на новий браслет і новий квиток. Посібник Supabase про сесії каже, що refresh tokens "never expire but can only be used once".

Refresh Token Not Found означає, що стійка шукає квиток і нічого не знаходить. Сервер відповідає HTTP 400 і кодом refresh_token_not_found, який список кодів помилок Auth у Supabase описує як "Session containing the refresh token not found." Якщо браслет теж сплив, JavaScript-клієнт Supabase прибирає сесію з браузера й повідомляє застосунку, що користувач вийшов. Саме тоді ваш користувач бачить сторінку входу.

Жоден із двох токенів не є API-ключем вашого проєкту, ключі це окреме питання: які з них мають бути у вашому застосунку. Якщо ваше повідомлення натомість каже Invalid JWT, відхилили браслет, а не квиток, і для цієї помилки є окрема сторінка. Якщо після цього людина не може знову увійти, а форма входу каже Invalid login credentials, за цим повідомленням стоять три різні людини.

Чому моїх користувачів Supabase викидає із системи?

Бо сесія, що стоїть за їхнім квитком, завершилася, а браслет, який вони ще носять, тримав їх у системі, доки не сплив. Ця затримка, до години за стандартного налаштування, і робить виходи схожими на випадкові: користувача викидає через якийсь час після того, як сесія завершилася.

Supabase Auth має чотири способи сказати, що квиток недійсний, і кожен вказує на іншу причину. Ось точні повідомлення з коду самого сервера Supabase Auth:

Повідомлення у вашому лозіКодЩо сталося
Invalid Refresh Token: Refresh Token Not Foundrefresh_token_not_foundСтійка не має запису про цей квиток. Його сесію завершив вихід, або його обміняли так давно, що запис уже зник.
Invalid Refresh Token: Already Usedrefresh_token_already_usedКвиток обміняли понад десять секунд тому, і він повернувся. Тоді Supabase завершує всю сесію.
Invalid Refresh Token: Session Expired, іноді з (Inactivity) або (Revoked by Newer Login) у кінціsession_expiredЇї завершив ліміт сесій у налаштуваннях Auth.
Invalid Refresh Token: No Valid Session Foundsession_not_foundСесії, якій належав квиток, більше немає.

За цими повідомленнями стоять три причини, згідно з документацією самого Supabase і звітами в його issue trackers, і кожна має своє виправлення. Якщо ніхто взагалі не може увійти або весь застосунок порожній, це інша проблема, і починати варто з чи впав Supabase, чи це ваш застосунок.

Refresh token працює один раз

Щоразу, коли застосунок поновлює сесію, він віддає квиток, і стійка рве старий. Supabase називає це ротацією refresh tokens. Старий квиток працює ще десять секунд, що посібник про сесії називає reuse interval, щоб сервер і браузер, які поновлюють сесію майже одночасно, не вибили одне одного. Клієнт, який обміняв квиток і втратив відповідь через погане з'єднання, теж захищений: він отримує поточний квиток назад.

Поза цими двома випадками старий квиток, що повертається, вважається тривожним сигналом, бо токен, який з'являється двічі, виглядає як украдений. Посібник каже, що тоді вся сесія "is regarded as terminated and all refresh tokens belonging to it are marked as revoked". Цього разу відповідь буде Already Used.

Ще один крок перетворює Already Used на Not Found. Сервер Supabase Auth стирає записи розірваних квитків через 24 години після їх відкликання. Тож застарілий квиток, який сьогодні отримує Already Used, завтра отримує Refresh Token Not Found, бо в стійки вже немає запису, з яким його порівняти.

Один квиток у чотири моменти. Протягом десяти секунд він ще працює. Потім його відхиляють як уже використаний, а коли його запис стирають через добу, стійка вже зовсім його не знаходить.

Причина 1: хтось вийшов, на цьому пристрої чи на іншому

Вихід видаляє сесію на боці стійки, і кожен квиток із неї стає таким, якого стійка не знаходить. У JavaScript-клієнті Supabase signOut() без нічого в дужках завершує всі сесії цього користувача на всіх пристроях. Посібник Supabase про вихід каже це прямо: за цього налаштування інші сесії падають з AuthApiError: Invalid Refresh Token: Refresh Token Not Found наступного разу, коли пробують поновитися, "even though no one signed out on that device."

Тож користувач, який вийшов на телефоні, вилітає й на ноутбуці, до години пізніше, коли браслет ноутбука спливає, а його квиток повертається як невідомий. З його боку здається, що ваш застосунок викинув його без причини.

Якщо кожен пристрій має лишатися в системі сам по собі, виклик виходу повинен завершувати тільки ту сесію, з якої його зроблено:

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

Попросіть свій білдер використовувати цю форму всюди, де застосунок виводить людей. Інший звичайний спосіб завершити сесію це зміна пароля, яку посібник Supabase про сесії називає серед того, що сесію завершує.

Без scope один вихід завершує сесію на всіх пристроях. Зі scope local він завершує лише ту, що на пристрої у вас у руці.

Причина 2: новий квиток так і не дійшов до браузера

Якщо ваш застосунок рендерить сторінки на сервері, як це робить застосунок на Next.js, цей сервер теж поновлює сесії. Він читає квиток із cookie, обмінює його на стійці й мусить повернути новий квиток браузеру в заголовку Set-Cookie своєї відповіді. Коли цей cookie не доходить, браузер лишається зі старим квитком, і наступний запит показує квиток, який сервер уже обміняв. Протягом десяти секунд це проходить. Після цього буде Already Used і сесія завершиться, а через добу той самий квиток уже Not Found.

Посібник Supabase про створення серверного клієнта називає помилку, яка до цього призводить у застосунку на Next.js. Код, що поновлює сесію, який у Next.js 16 називається Proxy, а в старіших версіях middleware, мусить повертати ту відповідь, у яку записано нові cookies: "An earlier response doesn't carry the refreshed cookies, so the user is signed out on the next request." Той самий посібник попереджає, що в Next.js 15 і старіших файл із назвою proxy.ts взагалі не запускається, "so sessions never refresh and users get signed out". Асистент, який працює з найновішою документацією, може записати саме такий файл у старіший застосунок.

Посібник Supabase також пояснює, чому цей випадок важко знайти: "it looks like users being signed out at random rather than an error in your code." Звіти звучать так само. Розробник написав в issue tracker @supabase/ssr "I'm inadvertently logged out of my web application every day", а інженер Supabase, який відповів, указав на те, як middleware передавав свою відповідь. Звіт про iOS-застосунок описує вихід "2-3 days after logging in", і його закрили після того, як оновлення Swift-бібліотеки Supabase зменшили кількість звітів.

Сервер поновив сесію, а новий квиток так і не повернувся. Браузер досі тримає той, який уже обміняли.

Щоб перевірити, відкрийте інструменти розробника в браузері на сторінці, яка поновила сесію, і подивіться на відповідь у вкладці Network. Вона має містити Set-Cookie з новим токеном, а наступний запит має надіслати цей токен назад.

Причина 3: ліміт сесій у налаштуваннях Auth

Починаючи з плану Pro, Supabase може завершувати сесії навмисно, зі сторінки Sessions у налаштуваннях Auth вашого проєкту. Є три ліміти, і посібник про сесії описує кожен:

  • Сесії з обмеженим часом завершуються через фіксований час, хоч би який активний був користувач.
  • Тайм-аут неактивності завершує сесію, яку якийсь час не поновлювали.
  • Одна сесія на користувача залишає лише найновіший вхід, тож вхід із телефона завершує сесію на ноутбуці.

Вони кажуть про це у власному повідомленні, Session Expired, з (Inactivity) або (Revoked by Newer Login) у кінці для другого й третього. Посібник додає, що завершені сесії видаляються через 24 години, тож квиток з такої сесії, який повертається після цього, отримує натомість Not Found.

Якщо ви ввімкнули один із них, виходи означають, що налаштування робить свою справу. Якщо ви цього не хотіли, або це ввімкнув за вас шаблон чи асистент, сторінка покаже, який ліміт увімкнено.

Нешкідливий випадок

Квиток із сесії, яка вже завершилася, що повертається зі старої вкладки або з браузера, якого тижнями ніхто не відкривав, теж отримує Refresh Token Not Found. Людину за ним уже вивели із системи. Її браузер запитав один раз, Supabase сказав ні, і вона бачить вашу сторінку входу, тобто те, що побачила б і так.

У лозі це виглядає так: один рядок для одного користувача, часто невдовзі після того, як цей користувач десь вийшов, і потім від нього більше нічого. Такі рядки означають, що помилка робить свою справу. Розбиратися варто з тими самими людьми знову і знову або з купою різних людей того самого дня.

Як читати логи Auth для цього

Відкрийте Logs у дашборді Supabase, виберіть тип логу Auth і пошукайте refresh token у повідомленнях подій. Виберіть рядок, щоб побачити подробиці, серед них ID користувача, а фільтр User потім звузить список до цієї однієї людини. Наскільки далеко сягають логи, залежить від вашого плану, тож читайте їх, поки скарги свіжі.

Потім читайте, що повторюється:

Що ви бачите в лозі AuthНа що це вказує
Один Refresh Token Not Found для користувача, і нічого післяСтара вкладка або браузер, що повернувся. Нешкідливо.
Refresh Token Not Found для користувача невдовзі після його виходу деіндеПричина 1, вихід, що завершив усі пристрої
Already Used, а через добу Refresh Token Not Found, для тих самих користувачів, знову і зновуПричина 2, нові квитки не доходять до браузера
Session Expired із причиною в дужкахПричина 3, ліміт сесій

Можна також запитати базу даних, як часто люди входять. Кожен вхід додає рядок до auth.sessions, а вихід його видаляє. Вставте це в 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;

Користувач, у якого сесій набагато більше, ніж пристроїв, входив знову і знову, ніколи не виходячи, а саме так поводиться той, кого ваш застосунок постійно викидає.

Чого не змінювати, поки виправляєте

Ваші правила Row Level Security, виявлення повторного використання і термін дії access token. Усі три схожі на виправлення й залишають причину на місці, а послаблення першого може відкрити ваші дані стороннім.

Не чіпайте свої правила Row Level Security. Row Level Security це набір правил, який вирішує, які рядки отримує кожен відвідувач. Поки користувачів викидає, кожен екран, чиє правило читає auth.uid(), для них порожній, бо згідно з документацією Supabaseauth.uid() повертає null, коли сесія сплила. Спокуслива ідея послабити це правило, доки екран знову не заповниться, але тоді він заповнюється й для сторонніх. Якщо хтось уже це зробив, наше безкоштовне сканування питає кожну таблицю, яку називає ваш застосунок, скільки рядків отримав би відвідувач без входу, читає число, не завантажуючи жодного рядка, і займає приблизно 20 секунд без акаунта: перевірте свій застосунок.

Залиште виявлення повторного використання ввімкненим. Supabase дозволяє його вимкнути, у розширених налаштуваннях на сторінці Sessions, і називає це "generally not recommended". Коли воно вимкнене, причина 2 більше не дає Already Used, а новий квиток усе одно не доходить до браузера.

Тримайте access token на одній годині. Довший браслет відкладає вихід і залишає його причину на місці. Посібник Supabase каже, що значення понад годину "generally discouraged for security reasons".

Щоб входи працювали й після виправлення

Дві речі ще можуть піти не так, коли виходи припиняться: пізніша зміна поверне їх, а правило, послаблене дорогою, лишить таблицю відкритою для сторонніх.

Reeve Monitor і далі перевіряє ззовні:

  • чи працює ваш застосунок, щохвилини, щоб хвилю повідомлень "мене викинуло" не сплутали зі збоєм
  • повна перевірка безпеки щогодини, до трьох застосунків
  • повідомлення, коли результат змінюється, щоб правило, послаблене під час виправлення, не чекало, поки ви глянете
  • щомісячний звіт простою мовою про те, що він бачив

Reeve Care тримає копію вашої бази даних Supabase:

  • зашифрована копія за розкладом, у власному сховищі Reeve в ЄС, поза вашим обліковим записом Supabase
  • схема auth, де живуть облікові записи ваших користувачів, щоб вони повернулися разом з усім іншим
  • кожна копія перевірена, перш ніж зарахуватися, підрахунком рядків у кожній таблиці
  • ваші вивантажені файли теж, щойно ви підключите доступ до Storage
  • відновлення в один клік, із безпечною копією поточного стану, зробленою заздалегідь
  • усе, що робить Monitor

Що зробити зараз

Що робити

  • Пошукайте refresh token у логах Auth і подивіться, яке повідомлення повторюється і в кого.
  • Якщо вихід на одному пристрої завершує інші, змініть його на signOut({ scope: 'local' }).
  • Якщо застосунок рендерить сторінки на сервері, переконайтеся, що відповідь, яка поновила сесію, і є тією, що доходить до браузера, і знайдіть її Set-Cookie у вкладці Network.
  • Перегляньте сторінку Sessions у налаштуваннях Auth: чи немає там обмеження часу, тайм-ауту неактивності або однієї сесії на користувача, які ніхто не збирався вмикати.
  • Залиште Row Level Security, виявлення повторного використання і термін дії access token як є, поки виправляєте.
  • Вважайте один Not Found на користувача шумом, а тих самих користувачів щодня багом.

Почніть із логу Auth, бо повідомлення, яке повторюється, скаже, яка з трьох причин у вас. Потім, коли люди перестануть вилітати, перевірте свій застосунок, щоб переконатися, що дорогою нічого не послабили.

Поширені запитання

Чому моїх користувачів Supabase викидає із системи без видимої причини?

Бо сесія, що стоїть за їхнім refresh token, завершилася, а access token, який у них ще є, тримає їх у системі до години. Тож вихід настає пізніше, ніж його причина. Звичні причини такі: вихід на іншому пристрої, поновлений токен, що так і не дійшов до браузера, і ліміт сесій у налаштуваннях Auth. Лог Auth показує, яка з них.

Що таке refresh token?

Це довготривала половина сесії Supabase. Access token за замовчуванням годину підтверджує, хто такий користувач, і до того, як він сплине, ваш застосунок обмінює refresh token на нову пару. Кожен refresh token працює один раз, і старий перестає працювати через десять секунд після обміну.

Чи "Refresh Token Not Found" ознака атаки?

Сам по собі ні. Він означає, що Supabase Auth відмовив токену, про який не має запису, і це правильна відповідь. Від украденого токена захищає виявлення повторного використання, яке завершує сесію, коли повертається старий токен, і Supabase радить лишати його ввімкненим.

Чому це трапляється лише в продакшені?

Бо більшості причин потрібен час або другий пристрій. Застарілий токен першого дня відхиляють як уже використаний, а як не знайдений, щойно його запис зітруть через добу, і сесія, у якій ви сидите під час розробки, рідко живе так довго. До того ж справжні користувачі входять із кількох пристроїв, і саме там видно вихід, що завершує всі сесії.

Скільки триває сесія Supabase?

За замовчуванням доти, доки користувач не вийде. Access token діє годину, якщо ви цього не змінили, а refresh token ніколи не спливає, проте працює лише один раз. Починаючи з плану Pro, сесії можна обмежити за загальним часом, за неактивністю або однією на користувача, на сторінці Sessions у налаштуваннях Auth.

Автор

Vlad Tkachenko

Засновник Reeve

Я щодня дивлюся на застосунки, зібрані в Lovable, Bolt, v0, Cursor і Replit, і на короткий список помилок, які трапляються в них знову і знову.

Більше про автора

Читати далі

Усі статті

Зробіть резервну копію бази Supabase перед наступною зміною

Reeve за розкладом зберігає зашифровану копію і відновить її за вас, якщо щось зламається.

Резервні копії Supabase

Автоматична зовнішня перевірка, а не повний аудит. Відсутність знахідок не є гарантією безпеки.