Skip to main content

Passwordless login via invite link

Discover how to implement an invite based sign up flow with the passwordless recipe.

Overview

In this flow, the admin of the app calls an API to sign up a user and send them an invite link. Once the user clicks on that, they log in and can access the app. If a user has not received an invitation before, their sign in attempt fails.

Before you start

This guide assumes that you have already implemented the EmailPassword recipe and have a working application integrated with SuperTokens. If you have not, please check the Quickstart Guide.

Steps

1. Add the ability to invite new users

Add a new endpoint that allows you to invite users to your app. You need to first create the new user and then use the passwordless API to send the magic link to them. Additionally, protect the endpoint with a role requirement.

The passwordless API uses the default magic link path, /auth/verify, for the invite link. If you are using the pre-built UI, the frontend SDK automatically logs the user in. For custom UI implementations, use the consumeCode function provided by the frontend SDK to call the passwordless API that verifies the code in the URL and creates the user.

import express from "express";
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { SessionRequest } from "supertokens-node/framework/express";
import UserRoles from "supertokens-node/recipe/userroles";
import Passwordless from "supertokens-node/recipe/passwordless";

let app = express();

app.post("/create-user", verifySession({
overrideGlobalClaimValidators: async function (globalClaimValidators) {
return [...globalClaimValidators,
UserRoles.UserRoleClaim.validators.includes("admin")]
}
}), async (req: SessionRequest, res) => {
let email = req.body.email;

// this will create the user in supertokens if they don't already exist.
await Passwordless.signInUp({
tenantId: "public",
email
})

let inviteLink = await Passwordless.createMagicLink({
tenantId: "public",
email
});

// TODO: send inviteLink to user's email
res.send("Success");
});
Multi Tenancy

In the above code snippets, the "public" tenantId passes when calling the functions - this is the default tenantId. If you are using the multi-tenancy feature, you can pass in a different tenantId and this ensures that the user with that email adds only to that tenant.

You also need to pass in the tenantId to the createMagicLink function which adds the tenantId to the generated magic link. The resulting link uses the websiteDomain configured in the appInfo object in SuperTokens.init, but you can change the link's domain to match that of the tenant before sending it.

2. Check if a user was invited

Update the backend SDK API function to only allow sign up requests from invited users. To do this you need to check if a user exists in SuperTokens.

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

Passwordless.init({
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
createCodePOST: async function (input) {
if ("email" in input) {
let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, {
email: input.email
});
let existingPasswordlessUser = existingUsers.find(user => user.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined);
if (existingPasswordlessUser === undefined) {
// this is sign up attempt
return {
status: "GENERAL_ERROR",
message: "Sign up disabled. Please contact the admin."
}
}
} else {
let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, {
phoneNumber: input.phoneNumber
});
let existingPasswordlessUser = existingUsers.find(user => user.loginMethods.find(lM => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless") !== undefined);
if (existingPasswordlessUser === undefined) {
// this is sign up attempt
return {
status: "GENERAL_ERROR",
message: "Sign up disabled. Please contact the admin."
}
}
}
return await originalImplementation.createCodePOST!(input);
}
}
}
}
})

See also