2025, Oct 21 09:16

Как сбросить пароль в Supabase из Python: token_hash и verify_otp

Пошаговый разбор сброса пароля в Supabase из Python: почему token из ссылки ломает verify_otp и как использовать token_hash для успешной проверки и update_user.

Сброс пароля пользователя Supabase из Python‑скрипта кажется простым, но легко упереться в сообщение «Token has expired or is invalid». Подвох в том, что токен из самой ссылки на сброс для проверки здесь не подходит. Решение — вывести в письме token_hash и передать его в verify_otp.

Постановка задачи

Скрипт инициализирует клиент Supabase, отправляет письмо для сброса пароля, достаёт токен из ссылки, пытается его проверить и затем сменить пароль. Ссылка приходит вида https://ABC.supabase.co/auth/v1/verify?token=XYZ&type=recovery&redirect_to=http://localhost:3000 — токен корректно извлекается, однако verify_otp возвращает «Token has expired or is invalid».

Воспроизводимый пример

Следующий код на Python показывает процесс от начала до конца, включая извлечение токена из полученного URL и попытку проверить его как recovery‑OTP.

import os
from supabase import create_client, Client
from dotenv import load_dotenv
load_dotenv()
# инициализировать клиент Supabase из переменных окружения
# Ожидается, что в окружении заданы SUPABASE_URL и SUPABASE_KEY
def build_client() -> Client:
    api_url: str = os.environ["SUPABASE_URL"]
    anon_key: str = os.environ["SUPABASE_KEY"]
    sb: Client = create_client(api_url, anon_key)
    return sb
def dispatch_reset_link(sb: Client):
    try:
        user_email = input("Please insert your email\n")
        resp = sb.auth.reset_password_for_email(
            user_email,
        )
        print("RESP=")
        print(resp)
        print("\nIf your email is already registered, you will receive a password reset email! Please check your inbox.\n")
    except Exception as exc:
        print("Failed to send reset email: ", str(exc), "\n")
def change_secret(sb: Client):
    try:
        link_raw = input("Please paste the link you received via email\n")
        email_input = input("Please insert your email\n")
        new_pass = input("Please insert your new password\n")
        # извлечь токен из URL
        parsed_token = link_raw.split("token=")[1].split("&type")[0]
        print("TOKEN = ", parsed_token)
        if not parsed_token:
            raise ValueError("Invalid link. No token found.")
        # проверить recovery‑OTP, используя токен из URL
        resp_verify = sb.auth.verify_otp({
            "email": email_input,
            "type": "recovery",
            "token": parsed_token,
        })
        print("RESP_1=")
        print(resp_verify)
        print("\n")
        # обновить пароль в аутентифицированной сессии восстановления
        resp_update = sb.auth.update_user({
            "password": new_pass
        })
        print("RESP_2=")
        print(resp_update)
        print("\n")
        print("Password updated successfully\n")
    except Exception as exc:
        print("Failed to update password: ", str(exc), "\n")
sb_client: Client = build_client()
print(sb_client)
print("\n\n")
dispatch_reset_link(sb_client)
change_secret(sb_client)

Почему проверка не проходит

В URL для восстановления действительно есть параметр token, но передача этого значения напрямую в verify_otp приводит к «Token has expired or is invalid». В этой схеме для проверки нужен именно token_hash, а не token из ссылки. Если пробовать передавать URL‑токен в verify_otp, проверка провалится, и до шага смены пароля вы не дойдёте.

Как исправить

Сначала измените шаблон письма так, чтобы он включал хэш токена. Затем прочитайте этот хэш и передайте его в verify_otp через поле token_hash.

<h2>Password Reset</h2>
<p>We're sorry to hear that you're having trouble accessing your account. To reset your password and regain access, please paste the token hash below into your terminal:</p>
<p>Token Hash: {{ .TokenHash }}</p>

В вашем Python‑коде примите и используйте хэш токена, указав type как recovery.

resp_1 = sb_client.auth.verify_otp({
  "type": "recovery",
  "token_hash": token_input,
})

Встраивая это в предыдущий скрипт, спрашивайте у пользователя именно хэш токена вместо парсинга токена из URL, а вызов смены пароля оставьте без изменений.

import os
from supabase import create_client, Client
from dotenv import load_dotenv
load_dotenv()
def build_client() -> Client:
    api_url: str = os.environ["SUPABASE_URL"]
    anon_key: str = os.environ["SUPABASE_KEY"]
    sb: Client = create_client(api_url, anon_key)
    return sb
def dispatch_reset_link(sb: Client):
    try:
        user_email = input("Please insert your email\n")
        resp = sb.auth.reset_password_for_email(user_email)
        print("RESP=")
        print(resp)
        print("\nIf your email is already registered, you will receive a password reset email! Please check your inbox.\n")
    except Exception as exc:
        print("Failed to send reset email: ", str(exc), "\n")
def change_secret(sb: Client):
    try:
        hashed_token = input("Please paste the token hash you received via email\n")
        new_pass = input("Please insert your new password\n")
        # выполнить проверку восстановления с использованием token_hash
        resp_verify = sb.auth.verify_otp({
            "type": "recovery",
            "token_hash": hashed_token,
        })
        print("RESP_1=")
        print(resp_verify)
        print("\n")
        # обновить пароль
        resp_update = sb.auth.update_user({
            "password": new_pass
        })
        print("RESP_2=")
        print(resp_update)
        print("\n")
        print("Password updated successfully\n")
    except Exception as exc:
        print("Failed to update password: ", str(exc), "\n")
sb_client: Client = build_client()
print(sb_client)
print("\n\n")
dispatch_reset_link(sb_client)
change_secret(sb_client)

Зачем это нужно

При построении сценариев восстановления пароля мелкие несоответствия между ожиданиями бэкенда и фактическими параметрами запроса приводят к непонятным ошибкам и сорванным сбросам. Использование token_hash выравнивает шаг проверки с тем самым токеном восстановления, который вы выводите в письме, поэтому процесс работает стабильно и для CLI‑скриптов, и для собственных интерфейсов, где пользователь вводит данные вручную.

Выводы

Если после извлечения токена из URL для восстановления ваш процесс рушится с «Token has expired or is invalid», перестаньте передавать token и перейдите на token_hash. Покажите хэш токена в письме для сброса и проверяйте его через verify_otp с type, установленным в recovery. После успешной проверки выполните update_user, чтобы задать новый пароль.

Статья основана на вопросе на StackOverflow от eljamba и ответе Andrew Smith.