Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to change email, OTP verification fails. "User not found" #798

Closed
loyoliteabid opened this issue Oct 16, 2023 · 14 comments
Closed

Unable to change email, OTP verification fails. "User not found" #798

loyoliteabid opened this issue Oct 16, 2023 · 14 comments
Labels
bug Something isn't working

Comments

@loyoliteabid
Copy link

Bug report

When a signed user attempts to change their email address. OTP receives to the new email. however when we try to verifyOtp I get an error saying "User not found". This is happening only when we try to change email using type: email_change. other types like signup are working fine.

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

  1. Signup using email
  2. Login using email and password
  3. change email using
    const { data, error } = await supabase.auth.updateUser({ email });
  4. Verify otp using
    const { data, error } = await supabase.auth.verifyOtp({ token: otp, type: "email_change", email, /* new email */ });
  5. See error
    {"name":"AuthApiError","message":"User not found","status":404}

Expected behavior

Signed user should be able to change their email with OTP verification

System information

-Browser chrome

  • "@supabase/auth-helpers-nextjs": "latest",
  • "@supabase/supabase-js": "latest",
@loyoliteabid loyoliteabid added the bug Something isn't working label Oct 16, 2023
@binaryArrow
Copy link

did you try it with type: 'email' instead of 'email_change'. For what exactly is the type 'email_change' used for?
I cannot find any explanation about this type in the docs.

@abiddraws
Copy link

abiddraws commented Oct 16, 2023

email_change is mentioned here https://supabase.com/docs/reference/javascript/auth-verifyotp , you can reproduce the issue from this code

"use client";
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";
import { useState } from "react";

export default function EmailChange() {
  const supabase = createClientComponentClient();

  const [email, setEmail] = useState("");
  const [otp, setOtp] = useState("");
  const [otpSent, setOtpSent] = useState(false);

  const handleSendOTP = async (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    e.stopPropagation();
    const { data, error } = await supabase.auth.updateUser({ email });

    if (error) {
      console.log("Error from updateEmail ", JSON.stringify(error));
    } else {
      console.log("Email updation success ", JSON.stringify(data));
      setOtpSent(true);
    }
  };

  const handleVerifyOTP = async (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log("Call receives...");
    e.preventDefault();
    e.stopPropagation();

    const { data, error } = await supabase.auth.verifyOtp({
      token: otp,
      type: "email_change",
      email,
    });

    if (error) {
      console.log(JSON.stringify(error));
    }
    if (data?.user) {
      alert(`OTP ${otp} verified successfully!`);
    }
  };

  return (
    <div className="max-w-md mx-auto p-6 border rounded-lg shadow-lg mt-10 bg-gray-50">
      <h2 className="text-2xl font-bold mb-4">OTP Verification</h2>

      <div className="mb-4">
        <label className="block text-gray-600">Email Address:</label>
        <input
          type="email"
          placeholder="Enter your email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          className="w-full p-2 border rounded-md focus:outline-none focus:ring focus:border-blue-300"
        />
      </div>

      {!otpSent ? (
        <button
          onClick={handleSendOTP}
          className="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring focus:border-blue-300"
        >
          Send OTP
        </button>
      ) : (
        <div>
          <div className="mb-4">
            <label className="block text-gray-600">Enter OTP:</label>
            <input
              type="text"
              placeholder="Enter OTP"
              value={otp}
              onChange={(e) => setOtp(e.target.value)}
              className="w-full p-2 border rounded-md focus:outline-none focus:ring focus:border-blue-300"
            />
          </div>
          <button
            onClick={handleVerifyOTP}
            className="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring focus:border-blue-300"
          >
            Verify OTP
          </button>
        </div>
      )}
    </div>
  );
}

@binaryArrow
Copy link

from looking at the docs, it does not exactly explain what it is used for, but by looking at your code, you might maybe refactor this snippet: const { data, error } = await supabase.auth.verifyOtp({ token: otp, type: "email_change", email, });
to this:
const { data, error } = await supabase.auth.verifyOtp({ token: otp, type: "email_change", email: email, });
because you do not use the properties in the order they are documented, I guess you have to specify the property names...

@abiddraws
Copy link

I have tried

const { data, error } = await supabase.auth.verifyOtp({
      token: otp,
      type: "email_change",
      email: email,
    });
and also tried this 
const { data, error } = await supabase.auth.verifyOtp({
   token: otp,
   type: 'email',
   email: email,
 });
Still same error

@binaryArrow
Copy link

Did you run the code with a User you know is definitely in the Users Table in Supabase? -> Does it still fails then?

@abiddraws
Copy link

Yes the user is in the supabase auth/user table. and it fails while verifyOtp

@saltcod saltcod transferred this issue from supabase/supabase Oct 17, 2023
@canardos
Copy link

canardos commented Oct 18, 2023

Are you using an older version of the local development environment? I was experiencing the same issue (local dev) and fixed it by upgrading to the latest CLI and local environment.

After looking through the GoTrue source I believe this was a bug in the verify endpoint, where it was not calculating the token_hash field from the token and email fields provided in the request (the token_hash is an SHA-256 hash of the email+otp). It seems to have been corrected about 3-months ago.

GoTrue matches the provided (new) email and token_hash fields to the email_change and email_change_token_new columns of the auth.users table, which get set after the initial updateUser call. This is why it was failing to find the user when the token_hash was missing.

I initially worked around the problem by manually calculating the token_hash on the client side and providing it in the verifyOtp call (token also required). This worked, but was not ideal. Updating to the latest version of everything solved the issue, and it now works as it is presumably intended.

An incorrect OTP will generate a user not found response for the reasons mentioned.

In case you want to test providing the hash manually:

updated: algo and comments

// Will need a lib for SHA-224
const buf = await window.crypto.subtle.digest("SHA-224", newEmail + otp);
const tokenHash = Array.from(new Uint8Array(buf))
    .map((item) => item.toString(16).padStart(2, "0"))
    .join("");

const { data, error } = await supabase.auth.verifyOtp({
// Should be only [type, token_hash ] or [type, email, token] on latest version
      type: "email_change",
      email: newEmail,
      token: otp,
      token_hash: tokenHash
});

For reference I'm (now) using v2.38 of the JS client and v1.106.1 of the CLI.

@abiddraws
Copy link

@canardos , I am utilizing Spabase Cloud rather than a local CLI. Nevertheless, when attempting your solution, I encountered the following error message:
{"name":"AuthApiError","message":"Verify requires either a token or a token hash","status":400}

code:

const handleVerifyOTP = async (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log("Call receives...");
    e.preventDefault();
    e.stopPropagation();

    const text = email + otp //'your data to hash';
    const encoder = new TextEncoder();
    const bufferText = encoder.encode(text);
    const buf = await window.crypto.subtle.digest("SHA-256", bufferText);
    const tokenHash = Array.from(new Uint8Array(buf))
      .map((item) => item.toString(16).padStart(2, "0"))
      .join("");

    const { data, error } = await supabase.auth.verifyOtp({
      token: otp,
      type: "email",
      email,
      token_hash: tokenHash,
    });

    if (error) {
      console.log(JSON.stringify(error));
    }
    if (data?.user) {
      alert(`OTP ${otp} verified successfully!`);
    }
  };

It expects either a "token" or a "token_hash."

The issue arises when I attempt to change my email using the OTP option. When I use the link method, it functions as expected. I want to emphasize that I am certain I'm entering the correct OTP, as I've attempted it multiple times.

I am using latest version of the library

 "dependencies": {
    "@supabase/auth-helpers-nextjs": "latest",
    "@supabase/supabase-js": "latest",
    "autoprefixer": "10.4.15",
    "next": "latest",
    "postcss": "8.4.29",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "tailwindcss": "3.3.3",
    "typescript": "5.1.3"
  },

@canardos
Copy link

Verify requires either a token or a token hash is the expected behavior when providing both the token and token_hash. I only had to provide both as I was using a version prior to this commit.

Given you're on the latest version, I suspect you have a different issue. You could try with the token_hash only, or use it to manually compare again the hash in the auth.users table, but I can't see it helping.

@abiddraws
Copy link

if I try with token_hash only , i get this error
{"name":"AuthApiError","message":"Database error finding user from email link","status":500}
:(

@canardos
Copy link

canardos commented Oct 19, 2023

Apologies, but my earlier code had the wrong algo (I'm using node (crypto.createHash("sha224").update(newEmail + otp).digest("hex").toLowerCase()) and copied/adjusted for the browser). It should be SHA-224. I don't think SHA-224 is supported in the browser, but you can easily just calculate it manually here and see if it matches the hash in the auth.users table. Or you could use a lib.

Also, you have type: "email", but is should be type: "email_change"

Finally, if you're providing the token_hash, then on the latest version I think you should only provide type and token_hash.

That said, it should be working with just type, email, and token, so clearly something else is wrong.

@kangmingtay
Copy link
Member

@abiddraws can you please send us a support ticket at https://supabase.com/dashboard/support/new ? it will be much easier for us to figure out what went wrong here with your project details!

@scottwyn13
Copy link

scottwyn13 commented Apr 1, 2024

I believe the issue is with the type "email_change". According to Supabase docs it states that the type is "email_change" but instead it should be "emailChange".
Screenshot 2024-04-01 at 1 37 01 PM

@kangmingtay
Copy link
Member

@scottwyn13 it should be email_change as defined in the types here -

export type EmailOtpType = 'signup' | 'invite' | 'magiclink' | 'recovery' | 'email_change' | 'email'

closing since i'm unable to reproduce this error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

6 participants