Skip to main content

OTP login

There are three parts to OTP login:

  • Creating and sending the OTP to the user.
  • Allowing the user to resend a (new) OTP if they want.
  • Validating the user's input OTP to login the user.
note

The same flow applies during sign up and sign in. If the user is signing up, the createdNewUser boolean on the frontend and backend will be true (as the result of the consume code API call).

Step 1: Creating and sending the OTP#

SuperTokens allows you to send an OTP to a user's email or phone number. You have already configured this setting on the backend SDK init function call in "Initialisation" section.

Start by making a form which asks the user for their email or phone, and then call the following API to create and send them an OTP.

import { createCode } from "supertokens-web-js/recipe/passwordless";

async function sendOTP(email: string) {
try {
let response = await createCode({
email
});
/**
* For phone number, use this:

let response = await createCode({
phoneNumber: "+1234567890"
});

*/

if (response.status === "SIGN_IN_UP_NOT_ALLOWED") {
// the reason string is a user friendly message
// about what went wrong. It can also contain a support code which users
// can tell you so you know why their sign in / up was not allowed.
window.alert(response.reason)
} else {
// OTP sent successfully.
window.alert("Please check your email for an OTP");
}
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you,
// or if the input email / phone number is not valid.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
}

Step 2: Resending a (new) OTP#

After sending the initial OTP to the user, you may want to display a resend button to them. When the user clicks on this button, you should call the following API

import { resendCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";

async function resendOTP() {
try {
let response = await resendCode();

if (response.status === "RESTART_FLOW_ERROR") {
// this can happen if the user has already successfully logged in into
// another device whilst also trying to login to this one.

// we clear the login attempt info that was added when the createCode function
// was called - so that if the user does a page reload, they will now see the
// enter email / phone UI again.
await clearLoginAttemptInfo();
window.alert("Login failed. Please try again");
window.location.assign("/auth")
} else {
// OTP resent successfully.
window.alert("Please check your email for the OTP");
}
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
}

How to detect if the user is in (Step 1) or in (Step 2) state?#

If you are building the UI for (Step 1) and (Step 2) on the same page, and if the user refreshes the page, you need a way to know which UI to show - the enter email / phone number form; or enter OTP + resend OTP form.

import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";

async function hasInitialOTPBeenSent() {
return await getLoginAttemptInfo() !== undefined;
}

If hasInitialOTPBeenSent returns true, it means that the user has already sent the initial OTP to themselves, and you can show the enter OTP form + resend OTP button (Step 2). Else show a form asking them to enter their email / phone number (Step 1).

Step 3: Verifying the input OTP#

When the user enters an OTP, you want to call the following API to verify it

import { consumeCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";

async function handleOTPInput(otp: string) {
try {
let response = await consumeCode({
userInputCode: otp
});

if (response.status === "OK") {
// we clear the login attempt info that was added when the createCode function
// was called since the login was successful.
await clearLoginAttemptInfo();
if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) {
// user sign up success
} else {
// user sign in success
}
window.location.assign("/home")
} else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") {
// the user entered an invalid OTP
window.alert("Wrong OTP! Please try again. Number of attempts left: " + (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount));
} else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") {
// it can come here if the entered OTP was correct, but has expired because
// it was generated too long ago.
window.alert("Old OTP entered. Please regenerate a new one and try again");
} else {
// this can happen if the user tried an incorrect OTP too many times.
// or if it was denied due to security reasons in case of automatic account linking

// we clear the login attempt info that was added when the createCode function
// was called - so that if the user does a page reload, they will now see the
// enter email / phone UI again.
await clearLoginAttemptInfo();
window.alert("Login failed. Please try again");
window.location.assign("/auth")
}
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
}
note

On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you.

See also#