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

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

"Invalid login credentials" у Supabase: три причини

Supabase відповідає "Invalid login credentials" на неправильний пароль, невідому пошту й акаунт Google. Як зрозуміти, що саме.

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

Коротко

  • Supabase відповідає "Invalid login credentials" трьом різним людям: тому, хто ввів неправильний пароль, тому, чия пошта не має облікового запису, і тому, хто зареєструвався через Google і ніколи не мав пароля.
  • Це зроблено навмисно. Форма входу, яка казала б, котрий із трьох випадків це був, повідомила б кожному, хто має список адрес, які з них належать вашим користувачам.
  • Розрізнити ці три випадки можна самому одним запитом до auth.users. Ваша сторінка входу має лишити повідомлення розмитим і запропонувати кожен спосіб, яким міг бути створений обліковий запис.

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

Invalid login credentials

Supabase відповідає "Invalid login credentials" на неправильний пароль. І рівно тими самими словами він відповідає ще двом людям: тому, чия пошта не має облікового запису у вашому застосунку, і тому, хто зареєструвався через Google і ніколи не мав пароля. Ваш застосунок із Lovable, Bolt, v0, Cursor, Claude Code або Replit працює так, як його збудовано. Він дає одну відповідь на три різні ситуації.

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

Що означає "Invalid login credentials" у Supabase?

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

Повідомлення записане в коді самого сервера Auth. У функції, що обробляє вхід за паролем, та сама помилка повертається в трьох місцях: коли жоден користувач не має цієї пошти, коли в користувача не збережено пароля і коли пароль не збігається. Щоразу відповідь має статус HTTP 400 і код invalid_credentials, який довідник кодів помилок Supabase описує як "Login credentials or grant type not recognized."

У вашому застосунку вона приходить як AuthApiError від signInWithPassword, де error.code дорівнює invalid_credentials. Той самий довідник радить завжди розпізнавати помилки за error.code та error.name, і це знадобиться далі, коли ви вирішуватимете, що каже ваша форма. Якщо ваші користувачі були залогінені, а потім їх викинуло, це інша помилка, і про неї є окрема сторінка.

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

Три людини за одним повідомленням

Кожному з трьох потрібне інше виправлення, тож варто знати, хто перед вами.

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

Адреса без облікового запису. Ніхто у вашому проєкті не реєструвався з цією поштою. Це може бути друга адреса (робоча, коли людина реєструвалася з особистою) або помилка під час реєстрації, яка тепер збережена як її пошта.

Обліковий запис без пароля. Людина зареєструвалася через Google, GitHub чи іншу кнопку входу, і Supabase ніколи не зберігав для неї пароля. Коли вона повертається й вводить пароль, порівнювати нема з чим. Сервер Auth перевіряє, чи пароль збережено, перш ніж дивитися на введений, і обліковий запис без пароля отримує той самий 400.

Ті, хто зареєструвався через magic link, тобто посилання для входу в листі, це різновид першого випадку. Код magic link у Supabase створює для них обліковий запис із випадковим паролем, якого ніхто ніколи не бачить, тож будь-який пароль, що вони вводять, неправильний.

Неправильний пароль, невідома адреса й обліковий запис без пароля доходять до тієї самої перевірки й отримують ту саму відповідь.

Як дізнатися, котрий це був випадок?

Запитайте власну базу даних. Кожен обліковий запис у вашому проєкті є рядком у таблиці auth.users, і цей рядок каже, як людина входить.

Посібник про користувачів Supabase описує поле, яке має значення: за словами Supabase, app_metadata.providers є списком провайдерів, через які користувач може входити. Користувач може мати кілька ідентичностей, тож одна людина може мати і пароль, і Google. Чи збережено пароль узагалі, видно з колонки, яку перевіряє сам сервер.

Відкрийте SQL Editor у панелі Supabase й виконайте це з адресою, яку дала людина:

select email,
       raw_app_meta_data->'providers' as signs_in_with,
       coalesce(encrypted_password, '') <> '' as has_password,
       email_confirmed_at
from auth.users
where lower(email) = lower('person@example.com');

Запит читає один рядок і ніколи не показує пароля, лише чи він існує.

Що повертаєтьсяЩо це означає
Жодного рядкаУ цьому проєкті немає облікового запису з такою адресою. Запитайте, яку ще адресу людина могла вказати
has_password дорівнює false, а signs_in_with містить google, github тощоЛюдина зареєструвалася через цю кнопку й ніколи не мала пароля
has_password дорівнює trueВведений пароль неправильний, або людина зареєструвалася через magic link і свого пароля не знала
email_confirmed_at порожнєПошту ще не підтверджено, і щойно пароль буде правильним, це стане іншим повідомленням

Якщо вам зручніше клацати, ніж писати, Authentication → Users у панелі показує ті самі облікові записи.

Обліковий запис, у якого ніколи не було пароля

Поверніть людину до кнопки, якою вона скористалася, або дайте їй задати пароль через ваше посилання для скидання.

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

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

Якщо людина хоче ще й пароль, ваше посилання "Забули пароль?" уже це робить. Код скидання пароля у Supabase надсилає лист для відновлення на будь-який обліковий запис, який знаходить, не перевіряючи, як його створили. Користувач Google, який перейде за посиланням і задасть новий пароль, далі зможе входити обома способами. Це залежить від того, чи є у вашому застосунку сторінка, яка задає новий пароль, тобто викликає supabase.auth.updateUser({ password }). Якщо посилання веде на сторінку, яка пароля ніколи не просить, людина один раз увійде й так і лишиться без пароля.

"Email not confirmed" означає, що пароль був правильний

Це інше повідомлення з іншим кодом, і воно доходить лише до того, хто вже пройшов перевірку пароля.

У тій самій функції сервера Auth перевірка підтвердженої пошти йде після перевірки пароля. Отже, Email not confirmed, код email_not_confirmed, повертається лише тому, хто ввів наявну пошту і правильний пароль до неї. Довідник помилок Supabase формулює це як "Signing in is not allowed for this user as the email address is not confirmed." Біля панелі це інша лампочка: код правильний, але двері лишаються зачиненими, доки не закінчено папери про заселення.

Ви бачите його, коли для провайдера Email у вашому проєкті ввімкнено Confirm email. За посібником із налаштування Supabase, користувачі тоді мають підтвердити адресу пошти, перш ніж увійти вперше. Виправлення в листі, який людина ще не натиснула: скажіть їй перевірити вхідні й спам і запропонуйте надіслати лист ще раз через supabase.auth.resend({ type: 'signup', email }). Якщо ці листи не приходять узагалі, обмеження на надсилання листів перше, що варто виключити. Якщо листи приходять, а посилання відповідає "Email link is invalid or has expired", найімовірніше, щось відкрило його раніше за людину.

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

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

Бо корисна версія розповіла б стороннім, хто ваші користувачі.

Уявіть, що панель відповідає "у квартирі 4Б ніхто не живе". Будь-хто, хто проходить повз, міг би пройтися списком квартир і дізнатися, які з них зайняті.

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

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

На боці реєстрації є перемикач, яким керуєте ви. Вигаданий користувач повертається тоді, коли підтвердження вимагається і для реєстрацій поштою, і для реєстрацій телефоном. Якщо будь-яке з двох вимкнено (телефонне враховується, навіть коли ваш застосунок ніколи не використовує вхід за телефоном), зареєстрована адреса отримує User already registered, код user_already_exists, і ваша форма реєстрації сама стає перевіркою списків.

До того ж корисне повідомлення має звідкись узятися. Supabase не каже вашому застосунку, чи є в пошти обліковий запис, тож, щоб показати "Облікового запису з цією поштою немає", застосунок має зʼясувати це сам. Це означає пошук, який працює для відвідувача без входу: читання вашої таблиці profiles, яке вдається лише тоді, коли будь-хто може читати пошту в ній, або функція бази даних, яка шукає в auth.users для будь-кого, хто питає. Так чи інакше, список ваших користувачів стає видимим ззовні.

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

Форма, яка називає відсутній обліковий запис, розкладає будь-який список адрес на ваших користувачів і всіх інших. Одна відповідь для всіх лишає список таким, яким він був.

Що натомість має бути на сторінці входу

Лишіть повідомлення розмитим і покладіть кожен спосіб повернутися на ту саму сторінку.

  • Одне повідомлення на всі три випадки, простими словами, з іншими шляхами поруч: "Пошта й пароль не збігаються. Якщо ви реєструвалися через Google, скористайтеся кнопкою нижче." Воно допомагає користувачу Google і не каже сторонньому нічого, чого не каже ваша сторінка реєстрації.
  • Кожен спосіб із вашої сторінки реєстрації. Кнопка Google, magic link і поле пароля, усе це й на сторінці входу.
  • Посилання "Забули пароль?" на сторінку, яка його задає. Supabase там відповідає на кожну адресу однаково, тож його безпечно показувати, і так людина без пароля його отримує.
  • Рішення за error.code. Показуйте своє розмите повідомлення для invalid_credentials, а "перевірте вхідні" з кнопкою повторного надсилання для email_not_confirmed. Довідник помилок Supabase радить розпізнавати помилки за кодом, а не за текстом повідомлення, і попереджає, що коди статусу HTTP можуть несподівано змінитися.

У коді це дві гілки:

const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error?.code === 'email_not_confirmed') showCheckYourInbox(email)
else if (error) showSignInHelp()

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

Щоб сторінка входу лишалася такою, якою ви її залишили

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

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

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

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

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

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

Що робити

  • Виконайте запит до auth.users з адресою, яку дала людина, і подивіться, котрий із трьох випадків це.
  • Жодного рядка: запитайте, з якою ще адресою людина могла зареєструватися.
  • Немає пароля: направте її до кнопки Google чи GitHub, якою вона користувалася, або до свого посилання для скидання.
  • Пароль є, а вхід усе одно відхилено: надішліть їй посилання для скидання.
  • Показуйте одне розмите повідомлення для invalid_credentials, а "перевірте вхідні" лише для email_not_confirmed.
  • Ніколи не додавайте перевірку, яка каже відвідувачу, чи є в пошти обліковий запис.

Якщо проблема починається на крок раніше і нові облікові записи взагалі не створюються, Database error saving new user описує цей бік форми. 10-хвилинний чекліст безпеки охоплює те, що зазвичай лишається відкритим у щойно запущеному застосунку.

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

Чому Supabase каже "Invalid login credentials", хоча пароль правильний?

Бо пароль може бути правильним для облікового запису, якого не існує, або в облікового запису може взагалі не бути пароля. Перевірте, чи це та сама пошта, з якою людина реєструвалася, і чи не реєструвалася вона через Google або іншу кнопку входу: тоді пароля в облікового запису немає. Один запит до auth.users у SQL Editor покаже, котрий це випадок.

Чи означає "Invalid login credentials", що облікового запису не існує?

Іноді. Supabase повертає те саме повідомлення з тим самим кодом, invalid_credentials, коли жоден обліковий запис не має цієї пошти, коли в облікового запису немає пароля і коли пароль неправильний. Саме повідомлення не каже, котрий випадок, і так його задумано.

Чому користувач, який зареєструвався через Google, не може увійти з паролем?

Бо його обліковий запис створено без пароля. Supabase перевіряє, чи пароль узагалі збережено, перш ніж порівнювати введений, і обліковий запис без пароля отримує "Invalid login credentials". Людина може й далі користуватися кнопкою Google або задати пароль через посилання для скидання у вашому застосунку: Supabase надсилає цей лист на будь-який обліковий запис, який знаходить, хоч би як його створили.

Чи має моя форма входу казати, що пошта не зареєстрована?

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

Чим "Invalid login credentials" відрізняється від "Email not confirmed"?

Supabase повертає "Email not confirmed" лише після того, як перевірив пароль і визнав його правильним. Отже, правильна людина ввела правильний пароль, а обліковий запис чекає, поки вона натисне посилання в листі з підтвердженням. Можна спокійно сказати їй перевірити вхідні й запропонувати надіслати лист ще раз.

Автор

Vlad Tkachenko

Засновник Reeve

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

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

Читати далі

Усі статті

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

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

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

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