Skip to main content

Passwordless login via allow list

We need to start by maintaining an allow list of emails. You can either store this list in your own database, or then use the metadata feature provided by SuperTokens to store this. This may seem like a strange use case of the user metadata recipe we provide, but it works.

You want to implement the following functions on your backend:

import UserMetadata from "supertokens-node/recipe/usermetadata"

async function addEmailToAllowlist(email: string) {
let existingData = await UserMetadata.getUserMetadata("emailAllowList");
let allowList: string[] = existingData.metadata.allowList || [];
allowList = [...allowList, email];
await UserMetadata.updateUserMetadata("emailAllowList", {
allowList
});
}

async function isEmailAllowed(email: string) {
let existingData = await UserMetadata.getUserMetadata("emailAllowList");
let allowList: string[] = existingData.metadata.allowList || [];
return allowList.includes(email);
}

async function addPhoneNumberToAllowlist(phoneNumber: string) {
let existingData = await UserMetadata.getUserMetadata("phoneNumberAllowList");
let allowList: string[] = existingData.metadata.allowList || [];
allowList = [...allowList, phoneNumber];
await UserMetadata.updateUserMetadata("phoneNumberAllowList", {
allowList
});
}

async function isPhoneNumberAllowed(phoneNumber: string) {
let existingData = await UserMetadata.getUserMetadata("phoneNumberAllowList");
let allowList: string[] = existingData.metadata.allowList || [];
return allowList.includes(phoneNumber);
}
important

Remember to initialise the user metadata recipe on the backend recipeList during supertokens.init.

After that, we override the createCodePOST API to check if the input email / phone number is allowed durign sign up. If not allowed, we send back a user friendly message to the frontend.

import Passwordless from "supertokens-node/recipe/passwordless";

Passwordless.init({
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
createCodePOST: async function (input) {
if ("email" in input) {
let existingUser = await Passwordless.getUserByEmail({
email: input.email
});
if (existingUser === undefined) {
// this is sign up attempt
if (!(await isEmailAllowed(input.email))) {
return {
status: "GENERAL_ERROR",
message: "Sign up disabled. Please contact the admin."
}
}
}
} else {
let existingUser = await Passwordless.getUserByPhoneNumber({
phoneNumber: input.phoneNumber
});
if (existingUser === undefined) {
// this is sign up attempt
if (!(await isPhoneNumberAllowed(input.phoneNumber))) {
return {
status: "GENERAL_ERROR",
message: "Sign up disabled. Please contact the admin."
}
}
}
}
return await originalImplementation.createCodePOST!(input);
}
}
}
}
})

createCodePOST is called when the user enters their email or phone number to login. We override it to check:

  • If there exists a user with the input email or phone number, it means they are signing in and so we allow the operation.
  • Otherwise, we check if the input email / phone number is allowed by calling our isEmailAllowed / isPhoneNumberAllowed function (which we implemented above). If not allowed, we return a message to the frontend.

We can add emails / phone numbers to the allow list by calling the addEmailToAllowlist / addPhoneNumberToAllowlist function we implemented above.

Which UI do you use?
Custom UI
Pre built UI