Passwordless login via invite link
In this flow, the admin of the app will call an API to sign up a user and send them on invite link. Once the user clicks on that, they will be logged in and can access the app. If a user has not been invited before, their sign in attempt will fail.
We start by overriding the createCodePOST
API to check if the input email / phone number was already invited before. If not, we send back a user friendly message to the frontend. We can check if a user was invited before by checking if they already exist in SuperTokens - cause users are created in SuperTokens when they successfully complete the invite flow.
- NodeJS
- GoLang
- Python
import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
ThirdPartyPasswordless.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { let existingUser = await ThirdPartyPasswordless.getUsersByEmail(input.email); if (existingUser.length === 0) { // this is sign up attempt return { status: "GENERAL_ERROR", message: "Sign up disabled. Please contact the admin." } } } else { let existingUser = await ThirdPartyPasswordless.getUserByPhoneNumber({ phoneNumber: input.phoneNumber }); if (existingUser === undefined) { // this is sign up attempt return { status: "GENERAL_ERROR", message: "Sign up disabled. Please contact the admin." } } } return await originalImplementation.createCodePOST!(input); } } } }})
import ( "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/recipe/thirdpartypasswordless" "github.com/supertokens/supertokens-golang/recipe/thirdpartypasswordless/tplmodels" "github.com/supertokens/supertokens-golang/supertokens")
func main() { thirdpartypasswordless.Init(tplmodels.TypeInput{ Override: &tplmodels.OverrideStruct{ APIs: func(originalImplementation tplmodels.APIInterface) tplmodels.APIInterface { originalCreateCodePOST := *originalImplementation.CreateCodePOST
(*originalImplementation.CreateCodePOST) = func(email, phoneNumber *string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) {
if email != nil { existingUser, err := thirdpartypasswordless.GetUsersByEmail(*email) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if len(existingUser) == 0 { // sign up attempt return plessmodels.CreateCodePOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Sign ups are disabled. Please contact the admin.", }, }, nil } } else { existingUser, err := thirdpartypasswordless.GetUserByPhoneNumber(*phoneNumber) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if existingUser == nil { // sign up attempt return plessmodels.CreateCodePOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Sign ups are disabled. Please contact the admin.", }, }, nil } } return originalCreateCodePOST(email, phoneNumber, options, userContext) }
return originalImplementation }, }, })}
from supertokens_python import init, InputAppInfofrom supertokens_python.types import GeneralErrorResponsefrom supertokens_python.recipe import thirdpartypasswordlessfrom supertokens_python.recipe.thirdpartypasswordless.asyncio import get_users_by_email, get_user_by_phone_numberfrom supertokens_python.recipe.thirdpartypasswordless.interfaces import APIInterface, CreateCodePostOkResult, PasswordlessAPIOptionsfrom typing import Union, Dict, Any
def override_thirdpartypasswordless_apis(original_implementation: APIInterface): original_create_code_post = original_implementation.create_code_post
async def create_code_post(email: Union[str, None], phone_number: Union[str, None], api_options: PasswordlessAPIOptions, user_context: Dict[str, Any], ) -> Union[CreateCodePostOkResult, GeneralErrorResponse]: if (email is not None): existing_user = await get_users_by_email(email) if len(existing_user) == 0: # sign up attempt return GeneralErrorResponse("Sign ups disabled. Please contact admin.") else: assert phone_number is not None existing_user = await get_user_by_phone_number(phone_number) if existing_user is None: # sign up attempt return GeneralErrorResponse("Sign ups disabled. Please contact admin.")
return await original_create_code_post(email, phone_number, api_options, user_context)
original_implementation.create_code_post = create_code_post return original_implementation
init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdpartypasswordless.init( flow_type="USER_INPUT_CODE", override=thirdpartypasswordless.InputOverrideConfig( apis=override_thirdpartypasswordless_apis, ), ) ])
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 it means that the user has not been invited to the app and we return an appropriate message to the frontend.
Now we will see how to make the API in which the admin of the app can create new users and invite them:
- NodeJS
- GoLang
- Python
- Express
- Hapi
- Fastify
- Koa
- Loopback
- AWS Lambda / Netlify
- Next.js
- NestJS
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 ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
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;
let inviteLink = await ThirdPartyPasswordless.createMagicLink({ email });
// TODO: send inviteLink to user's email res.send("Success");});
import Hapi from "@hapi/hapi";import { verifySession } from "supertokens-node/recipe/session/framework/hapi";import { SessionRequest } from "supertokens-node/framework/hapi";import UserRoles from "supertokens-node/recipe/userroles";import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
let server = Hapi.server({ port: 8000 });
server.route({ path: "/create-user", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } }) }, ], }, handler: async (req: SessionRequest, res) => { let email = (req.payload.valueOf() as any).email;
let inviteLink = await ThirdPartyPasswordless.createMagicLink({ email });
// TODO: send inviteLink to user's email res.response("Success").code(200); }})
import Fastify from "fastify";import { verifySession } from "supertokens-node/recipe/session/framework/fastify";import UserRoles from "supertokens-node/recipe/userroles";import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
let fastify = Fastify();
fastify.post("/create-user", { preHandler: verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } }),}, async (req, res) => { let email = req.body.email;
let inviteLink = await ThirdPartyPasswordless.createMagicLink({ email });
// TODO: send inviteLink to user's email res.code(200).send("Success");});
import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda";import { SessionEventV2 } from "supertokens-node/framework/awsLambda";import UserRoles from "supertokens-node/recipe/userroles";import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
async function createUser(awsEvent: SessionEventV2) { let email = JSON.parse(awsEvent.body!).email;
let inviteLink = await ThirdPartyPasswordless.createMagicLink({ email });
// TODO: send inviteLink to user's email return { statusCode: '200', body: "Success" }};
exports.handler = verifySession(createUser, { overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] }});
import KoaRouter from "koa-router";import { verifySession } from "supertokens-node/recipe/session/framework/koa";import { SessionContext } from "supertokens-node/framework/koa";import UserRoles from "supertokens-node/recipe/userroles";import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
let router = new KoaRouter();
router.post("/create-user", verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] }}), async (ctx: SessionContext, next) => { let email = (ctx.body as any).email;
let inviteLink = await ThirdPartyPasswordless.createMagicLink({ email });
// TODO: send inviteLink to user's email ctx.status = 200; ctx.body = "Success";});
import { inject, intercept } from "@loopback/core";import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest";import { verifySession } from "supertokens-node/recipe/session/framework/loopback";import UserRoles from "supertokens-node/recipe/userroles";import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
class LikeComment { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } @post("/create-user") @intercept(verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } })) async handler() { let email = "" // TODO: get from request body
let inviteLink = await ThirdPartyPasswordless.createMagicLink({ email });
// TODO: send inviteLink to user's email // TODO: send 200 response to the client }}
import { superTokensNextWrapper } from 'supertokens-node/nextjs'import { verifySession } from "supertokens-node/recipe/session/framework/express";import { SessionRequest } from "supertokens-node/framework/express";import UserRoles from "supertokens-node/recipe/userroles";import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
export default async function createUser(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } })(req, res, next); }, req, res )
let email = req.body.email;
let inviteLink = await ThirdPartyPasswordless.createMagicLink({ email });
// TODO: send inviteLink to user's email res.status(200).json({ message: 'Success' })}
import { Controller, Post, UseGuards, Session } from "@nestjs/common";import { SessionContainer } from "supertokens-node/recipe/session";import { AuthGuard } from './auth/auth.guard';import UserRoles from "supertokens-node/recipe/userroles";import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
@Controller()export class CreateUserController { @Post('create-user') @UseGuards(new AuthGuard({ overrideGlobalClaimValidators: async function (globalClaimValidators: any) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] } })) // For more information about this guard please read our NestJS guide. async postAPI(@Session() session: SessionContainer): Promise<void> { let email = "" // TODO: get from request body
let inviteLink = await ThirdPartyPasswordless.createMagicLink({ email });
// TODO: send inviteLink to user's email // TODO: send 200 response to the client }}
- Chi
- net/http
- Gin
- Mux
import ( "fmt" "net/http"
"github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/claims" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/recipe/thirdpartypasswordless" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens")
func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.PermissionClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, createUserAPI).ServeHTTP(rw, r) })}
func createUserAPI(w http.ResponseWriter, r *http.Request) { email := "" // TODO: read email from request body
inviteLink, err := thirdpartypasswordless.CreateMagicLinkByEmail(email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client}
import ( "fmt" "net/http"
"github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/claims" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/recipe/thirdpartypasswordless" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens")
func main() { router := gin.New()
// Wrap the API handler in session.VerifySession router.POST("/create-user", verifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.PermissionClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }), createUserAPI)}
// This is a function that wraps the supertokens verification function// to work the ginfunc verifySession(options *sessmodels.VerifySessionOptions) gin.HandlerFunc { return func(c *gin.Context) { session.VerifySession(options, func(rw http.ResponseWriter, r *http.Request) { c.Request = c.Request.WithContext(r.Context()) c.Next() })(c.Writer, c.Request) // we call Abort so that the next handler in the chain is not called, unless we call Next explicitly c.Abort() }}
func createUserAPI(c *gin.Context) { email := "" // TODO: read email from request body
inviteLink, err := thirdpartypasswordless.CreateMagicLinkByEmail(email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client}
import ( "fmt" "net/http"
"github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/claims" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/recipe/thirdpartypasswordless" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens")
func main() { r := chi.NewRouter()
// Wrap the API handler in session.VerifySession r.Post("/create-user", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.PermissionClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, createUserAPI))}
func createUserAPI(w http.ResponseWriter, r *http.Request) { email := "" // TODO: read email from request body
inviteLink, err := thirdpartypasswordless.CreateMagicLinkByEmail(email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client}
import ( "fmt" "net/http"
"github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/claims" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/recipe/thirdpartypasswordless" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens")
func main() { router := mux.NewRouter()
// Wrap the API handler in session.VerifySession router.HandleFunc("/create-user", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.PermissionClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, createUserAPI)).Methods(http.MethodPost)}
func createUserAPI(w http.ResponseWriter, r *http.Request) { email := "" // TODO: read email from request body
inviteLink, err := thirdpartypasswordless.CreateMagicLinkByEmail(email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client}
- FastAPI
- Flask
- Django
from supertokens_python.recipe.session.framework.fastapi import verify_sessionfrom supertokens_python.recipe.session import SessionContainerfrom fastapi import Dependsfrom supertokens_python.recipe.userroles import UserRoleClaimfrom supertokens_python.recipe.thirdpartypasswordless.asyncio import create_magic_link
@app.post('/create-user') async def create_user(session: SessionContainer = Depends(verify_session( override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")]))): email = "" # TODO: read from request body. invite_link = await create_magic_link(email, None)
print(invite_link) # TODO: send invite_link to email # TODO: send 200 responspe to client
from supertokens_python.recipe.session.framework.flask import verify_sessionfrom supertokens_python.recipe.userroles import UserRoleClaimfrom supertokens_python.recipe.thirdpartypasswordless.syncio import create_magic_link
@app.route('/create_user', methods=['POST']) @verify_session( override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")])def create_user(): email = "" # TODO: read from request body. invite_link = create_magic_link(email, None)
print(invite_link) # TODO: send invite_link to email # TODO: send 200 responspe to client
from supertokens_python.recipe.session.framework.django.asyncio import verify_sessionfrom django.http import HttpRequestfrom supertokens_python.recipe.userroles import UserRoleClaimfrom supertokens_python.recipe.thirdpartypasswordless.asyncio import create_magic_link
@verify_session( override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")])async def create_user(request: HttpRequest): email = "" # TODO: read from request body. invite_link = await create_magic_link(email, None)
print(invite_link) # TODO: send invite_link to email # TODO: send 200 responspe to client
- We guard the above API such that only signed in users with the
"admin"
role can call it. Feel free to change that part of the API. - The code above uses the default magic link path for the invite link (
/auth/verify
). If you are using the pre built UI, our frontend SDK will automatically log the user in. If you want to show a different UI to the user, then you can use a different path in the link (by modifying theinviteLink
string) and make your own UI on that path. If you are making your own UI, you can use theconsumeCode
function provided by our frontend SDK to call the passwordless API that verifies the code in the URL and creates the user. - You can change the lifetime of the magic link, and therefore the invite link, by following this guide.