Skip to main content

Magic link login

There are three parts to Magic link based login:

  • Creating and sending the magic link to the user.
  • Allowing the user to resend a (new) magic link if they want.
  • Consuming the link (when clicked) 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: Sending the Magic link#

SuperTokens allows you to send a magic link 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 a magic link

import { createPasswordlessCode } from "supertokens-web-js/recipe/thirdpartypasswordless";

async function sendMagicLink(email: string) {
try {
let response = await createPasswordlessCode({
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 {
// 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.");
}
}
}

Changing the magic link URL, or deep linking it to your app#

By default, the magic link will point to the websiteDomain that is configured on the backend, on the /auth/verify route (where /auth is the default value of websiteBasePath).

If you want to change this to a different path, a different domain, or deep link it to your mobile / desktop app, then you can do so on the backend in the following way:

import SuperTokens from "supertokens-node";
import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
import Session from "supertokens-node/recipe/session";

SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
recipeList: [
ThirdPartyPasswordless.init({
contactMethod: "EMAIL", // This example will work with any contactMethod
// This example works with the "USER_INPUT_CODE_AND_MAGIC_LINK" and "MAGIC_LINK" flows.
flowType: "USER_INPUT_CODE_AND_MAGIC_LINK",

emailDelivery: {
override: (originalImplementation) => {
return {
...originalImplementation,
sendEmail: async function (input) {
return originalImplementation.sendEmail({
...input,
urlWithLinkCode: input.urlWithLinkCode?.replace(
// This is: `${websiteDomain}${websiteBasePath}/verify`
"http://localhost:3000/auth/verify",
"http://your.domain.com/your/path"
)
})
}
}
}
}
}),
Session.init({ /* ... */ })
]
});
Multi Tenancy

For a multi tenant setup, the input to the sendEmail function will also contain the tenantId. You can use this to determine the correct value to set for the websiteDomain in the generated link.

Step 2: Resending a (new) 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 { resendPasswordlessCode, clearPasswordlessLoginAttemptInfo } from "supertokens-web-js/recipe/thirdpartypasswordless";

async function resendMagicLink() {
try {
let response = await resendPasswordlessCode();

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 clearPasswordlessLoginAttemptInfo();
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.");
}
}
}

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 the resend magic link form.

import { getPasswordlessLoginAttemptInfo } from "supertokens-web-js/recipe/thirdpartypasswordless";

async function hasInitialMagicLinkBeenSent() {
return await getPasswordlessLoginAttemptInfo() !== 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 (Step 2). Else show a form asking them to enter their email / phone number (Step 1).

Step 3: Consuming the Magic link#

This section talks about what needs to be done when the user clicks on the Magic link. There are two situations here:

  • The user clicks the Magic link on the same browser & device as the one they had started the flow on.
  • The user clicks the link on a different browser or device.

In order to detect which it is, you can do the following:

import { getPasswordlessLoginAttemptInfo } from "supertokens-web-js/recipe/thirdpartypasswordless";

async function isThisSameBrowserAndDevice() {
return await getPasswordlessLoginAttemptInfo() !== undefined;
}

If on the same device & browser#

On page load, you can consume the magic link by calling the following function

import { consumePasswordlessCode, clearPasswordlessLoginAttemptInfo } from "supertokens-web-js/recipe/thirdpartypasswordless";

async function handleMagicLinkClicked() {
try {
let response = await consumePasswordlessCode();

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 clearPasswordlessLoginAttemptInfo();
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 clearPasswordlessLoginAttemptInfo();
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.

If on a different device or browser#

In this case, you want to show some UI that requires a user interaction before consuming the magic link. This is to protect against email clients opening the magic link on their servers and consuming the link. For example, you could show a button with text like - "Click here to login into this device".

On click, you can consume the magic link to log the user into that device. Follow the instructions in the above section to know which function / API to call.

See also#