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 OTPSuperTokens 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.
- Web
- Mobile
- Via NPM
- Via Script Tag
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 createPasswordlessCode({
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.");
}
}
}
async function sendOTP(email: string) {
try {
let response = await supertokensPasswordless.createCode({
email
});
/**
* For phone number, use this:
let response = await supertokensPasswordless.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.");
}
}
}
- Single tenant setup
- Multi tenant setup
For email based login
curl --location --request POST '<YOUR_API_DOMAIN>/auth/signinup/code' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"email": "[email protected]"
}'
For phone number based login
curl --location --request POST '<YOUR_API_DOMAIN>/auth/signinup/code' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"phoneNumber": "+1234567890"
}'
For email based login
curl --location --request POST '<YOUR_API_DOMAIN>/auth/<TENANT_ID>/signinup/code' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"email": "[email protected]"
}'
For phone number based login
curl --location --request POST '<YOUR_API_DOMAIN>/auth/<TENANT_ID>/signinup/code' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"phoneNumber": "+1234567890"
}'
The response body from the API call has a status
property in it:
status: "OK"
: This means that the OTP was successfully sent.status: "GENERAL_ERROR"
: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend, or if the input email or password failed the backend validation logic.status: "SIGN_IN_UP_NOT_ALLOWED"
: This can happen during automatic account linking or during MFA. Thereason
prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed.
The response from the API call is the following object (in case of status: "OK"
):
{
status: "OK";
deviceId: string;
preAuthSessionId: string;
flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK";
fetchResponse: Response; // raw fetch response from the API call
}
You want to save the deviceId
and preAuthSessionId
on the frontend storage. These will be useful to:
- Resend a new OTP.
- Detect if the user has already sent an OTP before or if this is an entirely new login attempt. This distinction can be important if you have different UI for these two states. For example, if this info already exists, you do not want to show the user an input box to enter their email / phone, and instead want to show them the enter OTP form with a resend button.
- Verify the user's input OTP.
#
Step 2: Resending a (new) OTPAfter 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
- Web
- Mobile
- Via NPM
- Via Script Tag
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.");
}
}
}
async function resendOTP() {
try {
let response = await supertokensPasswordless.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 supertokensPasswordless.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.");
}
}
}
- Single tenant setup
- Multi tenant setup
curl --location --request POST '<YOUR_API_DOMAIN>/auth/signinup/code/resend' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"deviceId": "...",
"preAuthSessionId": "...."
}'
curl --location --request POST '<YOUR_API_DOMAIN>/auth/<TENANT_ID>/signinup/code/resend' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"deviceId": "...",
"preAuthSessionId": "...."
}'
The response body from the API call has a status
property in it:
status: "OK"
: This means that the OTP was successfully sent.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. You want to take the user back to the login screen where they can enter their email / phone number again. Be sure to remove the storeddeviceId
andpreAuthSessionId
from the frontend storage.status: "GENERAL_ERROR"
: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend.
#
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.
- Web
- Mobile
- Via NPM
- Via Script Tag
import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";
async function hasInitialOTPBeenSent() {
return await getLoginAttemptInfo() !== undefined;
}
async function hasInitialOTPBeenSent() {
return await supertokensPasswordless.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).
Since you save the preAuthSessionId
and deviceId
after the initial OTP is sent, you can know if the user is in (Step 1) vs (Step 2) by simply checking if these tokens are stored on the device.
If they aren't, you should follow (Step 1), else follow (Step 2).
important
You need to clear these tokens if the user navigates away from the (Step 2) page, or if you get a RESTART_FLOW_ERROR
at any point in time from an API call, or if the user has successfully logged in.
#
Step 3: Verifying the input OTPWhen the user enters an OTP, you want to call the following API to verify it
- Web
- Mobile
- Via NPM
- Via Script Tag
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.");
}
}
}
async function handleOTPInput(otp: string) {
try {
let response = await supertokensPasswordless.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 supertokensPasswordless.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 supertokensPasswordless.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.");
}
}
}
- Single tenant setup
- Multi tenant setup
curl --location --request POST '<YOUR_API_DOMAIN>/auth/signinup/code/consume' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"deviceId": "...",
"preAuthSessionId": "...",
"userInputCode": "<Entered OTP>"
}'
curl --location --request POST '<YOUR_API_DOMAIN>/auth/<TENANT_ID>/signinup/code/consume' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"deviceId": "...",
"preAuthSessionId": "...",
"userInputCode": "<Entered OTP>"
}'
The response body from the API call has a status
property in it:
status: "OK"
: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user.status: "INCORRECT_USER_INPUT_CODE_ERROR"
: The entered OTP is invalid. The response also contains information about the maximum number of retries and the number of failed attempts so far.status: "EXPIRED_USER_INPUT_CODE_ERROR"
: The entered OTP is too old. You should ask the user to resend a new OTP and try again.status: "RESTART_FLOW_ERROR"
: These responses that the user tried invalid OTPs too many times.status: "GENERAL_ERROR"
: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend.status: "SIGN_IN_UP_NOT_ALLOWED"
: This can happen during automatic account linking or during MFA. Thereason
prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed.
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.