Passwordless Setup for Magic Link Login and OTPs
Set up magic link login with the SuperTokens Passwordless recipe, or use email and SMS OTPs with prebuilt or custom UI.
Implement passwordless authentication with the right contact and flow options.
Implement SuperTokens passwordless authentication in this application. Inspect the existing frontend, backend, recipes, and routing first. Ask whether users should authenticate through email, SMS, or both, and whether the flow should use magic links, OTPs, or both. Configure the frontend and backend Passwordless and Session recipes, the selected UI, auth routes, and email or SMS delivery. Preserve existing conventions, keep credentials in environment variables, and validate sign-in, resend, expiry, and session behavior.
Overview
This page shows you how to add the Passwordless recipe to your project.
The tutorial creates a login flow, rendered by either the Prebuilt UI components or by your own Custom UI.
Terminology
Before going into the actual steps lets first talk about two terms that influence how you configure the Passwordless recipe.
- Contact Method: This defines how the user receives the credentials from your app. You can choose between
email,phone numberor both (the user has to choose one during the login flow). - Flow Type: This is the credential type used for authentication. You can choose Magic Link, OTP (One-Time Password), or both. The combined flow sends both credentials and the user can complete authentication with either one.
Steps
1. Initialize the frontend SDK
1.1 Add the Passwordless recipe in your main configuration file.
Add the Passwordless recipe in your AuthComponent.
Add the Passwordless recipe in your AuthView file.
import React from "react";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import Passwordless from "supertokens-auth-react/recipe/passwordless";
import Session from "supertokens-auth-react/recipe/session";
SuperTokens.init({
appInfo: {
// learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth",
},
recipeList: [
Passwordless.init({
contactMethod: "EMAIL",
}),
Session.init(),
],
});import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core";
import { DOCUMENT } from "@angular/common";
@Component({
selector: "app-auth",
template: '<div id="supertokensui"></div>',
})
export class AuthComponent implements OnDestroy, AfterViewInit {
constructor(
private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document,
) {}
ngAfterViewInit() {
this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@vX.Y.Z/build/static/js/main.test.js");
}
ngOnDestroy() {
// Remove the script when the component is destroyed
const script = this.document.getElementById("supertokens-script");
if (script) {
script.remove();
}
}
private loadScript(src: string) {
const script = this.renderer.createElement("script");
script.type = "text/javascript";
script.src = src;
script.id = "supertokens-script";
script.onload = () => {
supertokensUIInit({
appInfo: {
// learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth",
},
recipeList: [
supertokensUIPasswordless.init({
contactMethod: "EMAIL",
}),
supertokensUISession.init(),
],
});
};
this.renderer.appendChild(this.document.body, script);
}
}<script lang="ts">
import { defineComponent, onMounted, onUnmounted } from "vue";
export default defineComponent({
setup() {
const loadScript = (src: string) => {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = src;
script.id = "supertokens-script";
script.onload = () => {
supertokensUIInit({
appInfo: {
// learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth",
},
recipeList: [
supertokensUIPasswordless.init({
contactMethod: "EMAIL",
}),
supertokensUISession.init(),
],
});
};
document.body.appendChild(script);
};
onMounted(() => {
loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@vX.Y.Z/build/static/js/main.test.js");
});
onUnmounted(() => {
const script = document.getElementById("supertokens-script");
if (script) {
script.remove();
}
});
},
});
</script>
<template>
<div id="supertokensui" />
</template>1.2 Include the pre-built UI components in your application.
To render the Pre-Built UI inside your application, you need to specify which routes show the authentication components. The React SDK uses React Router under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes.
import React from "react";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import * as reactRouterDom from "react-router-dom";
class App extends React.Component {
render() {
return (
<SuperTokensWrapper>
<BrowserRouter>
<Routes>
{/*This renders the login UI on the /auth route*/}
{getSuperTokensRoutesForReactRouterDom(reactRouterDom, [PasswordlessPreBuiltUI])}
{/*Your app routes*/}
</Routes>
</BrowserRouter>
</SuperTokensWrapper>
);
}
}import React from "react";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui";
import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui";
class App extends React.Component {
render() {
if (canHandleRoute([PasswordlessPreBuiltUI])) {
// This renders the login UI on the /auth route
return getRoutingComponent([PasswordlessPreBuiltUI]);
}
return <SuperTokensWrapper>{/*Your app*/}</SuperTokensWrapper>;
}
}import React from "react";
import { BrowserRouter, useRoutes } from "react-router-dom";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import * as reactRouterDom from "react-router-dom";
function AppRoutes() {
const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [
/* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */
]);
const routes = useRoutes([
...authRoutes.map((route) => route.props),
// Include the rest of your app routes
]);
return routes;
}
function App() {
return (
<SuperTokensWrapper>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</SuperTokensWrapper>
);
}2. Initialize the backend SDK
You need to initialize the Backend SDK alongside the code that starts your server. The init call includes configuration details for your app. It specifies how the backend connects to the SuperTokens Core, as well as the Recipes used in your setup.
For the Passwordless recipe, you also need to specify the flowType and contactMethod.
Click one of the options from the next form and the code snippet updates.
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import Passwordless from "supertokens-node/recipe/passwordless";
supertokens.init({
// Replace this with the framework you are using
framework: "express",
supertokens: {
// We use try.supertokens for demo purposes.
// At the end of the tutorial we will show you how to create
// your own SuperTokens core instance and then update your config.
connectionURI: "https://try.supertokens.io",
// apiKey: <YOUR_API_KEY>
},
appInfo: {
// learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth",
},
recipeList: [
Passwordless.init({
flowType: "MAGIC_LINK",
contactMethod: "EMAIL",
}),
Session.init(),
],
});from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import passwordless, session
from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig
init(
app_info=InputAppInfo(
app_name="<YOUR_APP_NAME>",
api_domain="<YOUR_API_DOMAIN>",
website_domain="<YOUR_WEBSITE_DOMAIN>",
api_base_path="/auth",
website_base_path="/auth"
),
supertokens_config=SupertokensConfig(
# We use try.supertokens for demo purposes.
# At the end of the tutorial we will show you how to create
# your own SuperTokens core instance and then update your config.
connection_uri="https://try.supertokens.io",
# api_key: <YOUR_API_KEY>
),
framework='fastapi',
recipe_list=[
session.init(), # initializes session features
passwordless.init(
flow_type="MAGIC_LINK",
contact_config=ContactEmailOnlyConfig()
)
],
mode='asgi' # use wsgi if you are running using gunicorn
)import (
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/passwordless"
"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
apiBasePath := "/auth"
websiteBasePath := "/auth"
err := supertokens.Init(supertokens.TypeInput{
Supertokens: &supertokens.ConnectionInfo{
// We use try.supertokens for demo purposes.
// At the end of the tutorial we will show you how to create
// your own SuperTokens core instance and then update your config.
ConnectionURI: "https://try.supertokens.io",
// APIKey: <YOUR_API_KEY>
},
AppInfo: supertokens.AppInfo{
AppName: "<YOUR_APP_NAME>",
APIDomain: "<YOUR_API_DOMAIN>",
WebsiteDomain: "<YOUR_WEBSITE_DOMAIN>",
APIBasePath: &apiBasePath,
WebsiteBasePath: &websiteBasePath,
},
RecipeList: []supertokens.Recipe{
passwordless.Init(plessmodels.TypeInput{
FlowType: "MAGIC_LINK",
ContactMethodEmail: plessmodels.ContactMethodEmailConfig{Enabled: true},
}),
session.Init(nil), // initializes session features
},
})
if err != nil {
panic(err.Error())
}
}1. Initialize the frontend SDK
Call the SDK init function at the start of your application. The invocation includes the main configuration details, as well as the recipes that you use in your setup.
First, you need to add the recipe script tag.
Add the SuperTokens.init function call at the start of your application.
import SuperTokens from "supertokens-web-js";
import Session from "supertokens-web-js/recipe/session";
import Passwordless from "supertokens-web-js/recipe/passwordless";
SuperTokens.init({
appInfo: {
apiDomain: "<YOUR_API_DOMAIN>",
apiBasePath: "/auth",
appName: "...",
},
recipeList: [Session.init(), Passwordless.init()],
});<script src="https://cdn.jsdelivr.net/gh/supertokens/supertokens-web-js@vX.Y.Z/bundle/passwordless.test.js"></script>import SuperTokens from "supertokens-react-native";
SuperTokens.init({
apiDomain: "<YOUR_API_DOMAIN>",
apiBasePath: "/auth",
});import android.app.Application
import com.supertokens.session.SuperTokens
class MainApplication: Application() {
override fun onCreate() {
super.onCreate()
SuperTokens.Builder(this, "<YOUR_API_DOMAIN>")
.apiBasePath("/auth")
.build()
}
}import UIKit
import SuperTokensIOS
fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
try SuperTokens.initialize(
apiDomain: "<YOUR_API_DOMAIN>",
apiBasePath: "/auth"
)
} catch SuperTokensError.initError(let message) {
// TODO: Handle initialization error
} catch {
// Some other error
}
return true
}
}import 'package:supertokens_flutter/supertokens.dart';
void main() {
SuperTokens.init(
apiDomain: "<YOUR_API_DOMAIN>",
apiBasePath: "/auth",
);
}You can initialize the SDK
supertokens.init({
appInfo: {
apiDomain: "<YOUR_API_DOMAIN>",
apiBasePath: "/auth",
appName: "...",
},
recipeList: [supertokensSession.init(), supertokensPasswordless.init()],
});2. Add the login UI
Follow the section that matches your configured flowType. For USER_INPUT_CODE_AND_MAGIC_LINK, one create-code request sends both a magic link and an OTP. Use the shared create and resend behavior from steps 2.1 and 2.2, then implement both consumption paths so the user can complete either one.
Magic Link
The following section shows you what aspects you need to cover to implement the UI for a Magic Link flow.
The same flow applies during either sign up or sign in.
This guide shows you how to determine if the system creates a new user in the next steps.
2.1 Sending the Magic link
You need to add a form that asks the user for their email address or phone number. When the user submits the form, you need to call the following API to create and send them a Magic Link.
For email based login
import { createCode } from "supertokens-web-js/recipe/passwordless";
async function sendMagicLink(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 {
// Magic link sent successfully.
window.alert("Please check your email for the magic link");
}
} 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 sendMagicLink(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 {
// Magic link sent successfully.
window.alert("Please check your email for the magic link");
}
} 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.");
}
}
}curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/signinup/code' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"email": "johndoe@gmail.com"
}'For phone number based login
curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/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 magic link was successfully sent.status: "GENERAL_ERROR": This is possible if you have overridden the backend API to send back a custom error message which should display 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 multi-factor authentication (MFA). Thereasonprop 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";
}You want to save the deviceId and preAuthSessionId on the frontend storage. These are useful to:
- Resend a new magic link.
- Detect if the user has already sent a magic link 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 resend link button.
2.2 Resending a magic link
After sending the initial magic link 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 resendMagicLink() {
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 {
// Magic link resent successfully.
window.alert("Please check your email for the magic link");
}
} 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 resendMagicLink() {
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 {
// Magic link resent successfully.
window.alert("Please check your email for the magic link");
}
} 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.");
}
}
}curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/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 magic link 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 storeddeviceIdandpreAuthSessionIdfrom the frontend storage.status: "GENERAL_ERROR": This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend.
How to detect if the initial OTP has been sent
If you are building the send and enter OTP interfaces on the same page, you might run into an issue when the user refreshes the page. To prevent this you need a way to know which UI to show.
Since you save the preAuthSessionId and deviceId after sending the initial magic link, you can know if the user is on either step 2.1 or step 2.2. Check if these tokens are on the device.
If they aren’t, you should follow step 2.1, else follow step 2.2.
import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";
async function hasInitialMagicLinkBeenSent() {
return (await getLoginAttemptInfo()) !== undefined;
}async function hasInitialMagicLinkBeenSent() {
return (await supertokensPasswordless.getLoginAttemptInfo()) !== undefined;
}If hasInitialMagicLinkBeenSent returns true, it means that the user has already sent the initial magic link to themselves, and you can show the resend link UI. Else show a form asking them to enter their email / phone number.
2.3 Consuming the magic link
When a user clicks on a magic link, you first need to know if the action came from the same browser/device as the one that started the flow. To do this you ca use this code sample.
Since you save the preAuthSessionId and deviceId, you can check if they exist on the app. If they do, then it’s the same device that the user has opened the link on, else it’s a different device.
import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";
async function isThisSameBrowserAndDevice() {
return (await getLoginAttemptInfo()) !== undefined;
}async function isThisSameBrowserAndDevice() {
return (await supertokensPasswordless.getLoginAttemptInfo()) !== undefined;
}If the user clicked on a link from a different device, you need to show some kind of an intermediate UI. This is to protect against email clients opening the magic link on their servers and consuming the link.
The page should require additional user interaction before consuming the magic link.
For example, you could show a button with the following text: Click here to login into this device.
On click, you can consume the magic link to log the user into that device.
With this understanding of how to avoid potential errors, proceed with the actual instructions on how to authenticate with the magic link.
You need to remove the linkCode and preAuthSessionId from the Magic link. For example, if the Magic link is
import { consumeCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";
async function handleMagicLinkClicked() {
try {
let response = await consumeCode();
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 {
// this can happen if the magic link has expired or is invalid
// 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 handleMagicLinkClicked() {
try {
let response = await supertokensPasswordless.consumeCode();
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 {
// this can happen if the magic link has expired or is invalid
// 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.");
}
}
}https://example.com/auth/verify?preAuthSessionId=PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=#s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=Then the preAuthSessionId is the value of the query parameter preAuthSessionId (PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s= in the example), and the linkCode is the part after the # (s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs= in the example).
We can then use these to call the consume API
curl --location --request POST '<YOUR_API_DOMAIN>/auth/<TENANT_ID>/signinup/code/consume' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"linkCode": "s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=",
"preAuthSessionId": "PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s="
}'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" | "EXPIRED_USER_INPUT_CODE_ERROR" | "RESTART_FLOW_ERROR": These responses indicate that the Magic link was invalid or expired.status: "GENERAL_ERROR": This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend.status: "SIGN_IN_UP_NOT_ALLOWED": This can happen during automatic account linking or during multi-factor authentication (MFA). Thereasonprop that’s in the response body contains a support code using which you can see why the sign in / up was not allowed.
OTP
The following section shows you what aspects you need to cover to implement the UI for a OTP, One-Time Password, flow.
The same flow applies during either sign up or sign in.
This guide shows you how to determine if you create a new user in the next steps.
2.1 Creating and sending the OTP
You have to add a form that asks the user for their email address or phone number. When the users submit the form you have to call the following API to create and send them an OTP.
For email based login
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.");
}
}
}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.");
}
}
}curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/signinup/code' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"email": "johndoe@gmail.com"
}'For phone number based login
curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/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 overridden the backend API to send back a custom error message which should display 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 duringMFA. Thereasonprop 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";
}You want to save the deviceId and preAuthSessionId on the frontend storage. These are 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.
2.2 Resending a OTP
After you send the 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.");
}
}
}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.");
}
}
}curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/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 storeddeviceIdandpreAuthSessionIdfrom the frontend storage.status: "GENERAL_ERROR": This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend.
How to detect if the initial OTP has been sent
If you are building the send and enter OTP interfaces on the same page, you might run into an issue when the user refreshes the page. To prevent this you need a way to know which UI to show.
Since you save the preAuthSessionId and deviceId after sending the initial OTP, you can determine which interface to show.
Check if you stored these tokens on the device.
If they aren’t present, show the form from step 2.1. Otherwise, show the OTP form from step 2.3 with the resend action from step 2.2.
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, show the OTP form from step 2.3 with the resend action from step 2.2. Otherwise, show the form from step 2.1 asking users to enter their email or phone number.
2.3 Verifying the OTP
When the user enters an OTP you have 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.");
}
}
}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.");
}
}
}curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/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 contains information about the maximum number of retries and the number of failed attempts.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": The user entered invalid OTPs too many times and must restart the flow.status: "GENERAL_ERROR": This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend.status: "SIGN_IN_UP_NOT_ALLOWED": This can happen during automatic account linking or duringMFA. Thereasonprop that’s in the response body contains a support code using which you can see why the sign in / up was not allowed.
On success, the backend sends session tokens in the response. Web SDK requests handle them automatically. Native SDKs only do so when the request uses their integrated HTTP client or interceptor; raw requests such as the curl examples must be implemented through that integration in the app.
3. Initialize the backend SDK
You need to initialize the Backend SDK alongside the code that starts your server. The init call includes configuration details for your app. It specifies how the backend connects to the SuperTokens Core, as well as the Recipes used in your setup.
For the Passwordless recipe, you also need to specify the flowType and contactMethod.
Click one of the options from the next form and the code snippet updates.
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import Passwordless from "supertokens-node/recipe/passwordless";
supertokens.init({
// Replace this with the framework you are using
framework: "express",
supertokens: {
// We use try.supertokens for demo purposes.
// At the end of the tutorial we will show you how to create
// your own SuperTokens core instance and then update your config.
connectionURI: "https://try.supertokens.io",
// apiKey: <YOUR_API_KEY>
},
appInfo: {
// learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth",
},
recipeList: [
Passwordless.init({
flowType: "MAGIC_LINK",
contactMethod: "EMAIL",
}),
Session.init(),
],
});from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import passwordless, session
from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig
init(
app_info=InputAppInfo(
app_name="<YOUR_APP_NAME>",
api_domain="<YOUR_API_DOMAIN>",
website_domain="<YOUR_WEBSITE_DOMAIN>",
api_base_path="/auth",
website_base_path="/auth"
),
supertokens_config=SupertokensConfig(
# We use try.supertokens for demo purposes.
# At the end of the tutorial we will show you how to create
# your own SuperTokens core instance and then update your config.
connection_uri="https://try.supertokens.io",
# api_key: <YOUR_API_KEY>
),
framework='fastapi',
recipe_list=[
session.init(), # initializes session features
passwordless.init(
flow_type="MAGIC_LINK",
contact_config=ContactEmailOnlyConfig()
)
],
mode='asgi' # use wsgi if you are running using gunicorn
)import (
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/passwordless"
"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
apiBasePath := "/auth"
websiteBasePath := "/auth"
err := supertokens.Init(supertokens.TypeInput{
Supertokens: &supertokens.ConnectionInfo{
// We use try.supertokens for demo purposes.
// At the end of the tutorial we will show you how to create
// your own SuperTokens core instance and then update your config.
ConnectionURI: "https://try.supertokens.io",
// APIKey: <YOUR_API_KEY>
},
AppInfo: supertokens.AppInfo{
AppName: "<YOUR_APP_NAME>",
APIDomain: "<YOUR_API_DOMAIN>",
WebsiteDomain: "<YOUR_WEBSITE_DOMAIN>",
APIBasePath: &apiBasePath,
WebsiteBasePath: &websiteBasePath,
},
RecipeList: []supertokens.Recipe{
passwordless.Init(plessmodels.TypeInput{
FlowType: "MAGIC_LINK",
ContactMethodEmail: plessmodels.ContactMethodEmailConfig{Enabled: true},
}),
session.Init(nil), // initializes session features
},
})
if err != nil {
panic(err.Error())
}
}Next steps
Having completed the main setup, you can explore more advanced topics related to the Passwordless recipe.
Customize the Magic Link
Change how Magic Links get created.
OTP Customization
Change the format of the generated One-Time Password.
Hooks and overrides
Add custom logic after the logs in or signs up.
Email Delivery
Customize how emails get delivered to your users.
SMS Delivery
Customize how SMS messages get delivered to your users.