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