3) Passwordless customisation
This recipe will provide APIs on the backend which can be used to implement the second login challenge - verifying the phone number via an OTP.
Start by following the backend quick setup guide for the passwordless recipe, and setting the contactMethod
to "PHONE"
and setting flowType
to "USER_INPUT_CODE"
.
import Passwordless from "supertokens-node/recipe/passwordless";import supertokens from "supertokens-node";
supertokens.init({ framework: "...", appInfo: { /*...*/ }, recipeList: [ Passwordless.init({ contactMethod: "PHONE", flowType: "USER_INPUT_CODE", }) ]})
#
Check the phone number in the code creation APIThe first step to verifying the phone number is to send a SMS code to the phone number. The passwordless recipe provides an API for that: {apiBasePath}/signinup/code POST
. We want to modify this API to check that the input phone number is the same as the one that was stored in the session. This will prevent malicious users from verifying a different phone number that the one they entered in the first login challenge:
import Passwordless from "supertokens-node/recipe/passwordless";import supertokens from "supertokens-node";import Session from "supertokens-node/recipe/session";
supertokens.init({ framework: "...", appInfo: { /*...*/ }, recipeList: [ Passwordless.init({ contactMethod: "PHONE", flowType: "USER_INPUT_CODE", override: { apis: (oI) => { return { ...oI, createCodePOST: async function (input) { if (oI.createCodePOST === undefined) { throw new Error("Should never come here"); } /** * * We want to make sure that the OTP being generated is for the * same number that was used in the first login challenge. Otherwise * someone could "hack" the frontend to change the phone number * being sent for the second login challenge. */
/* We pass overrideGlobalClaimValidators: () => [], so that the PhoneVerifiedClaim claim check does not happen, cause if it does, getSession will throw an error since we have not yet verified the user's phone number. */ let session = await Session.getSession(input.options.req, input.options.res, { overrideGlobalClaimValidators: () => [], });
/* We had saved the phone number in the session in the previous customisation step. */ let phoneNumber: string = session.getAccessTokenPayload().phoneNumber;
if (!("phoneNumber" in input) || input.phoneNumber !== phoneNumber) { /*This means that the phone number that this API got was not the same one as entered in the first challenge. It should come here only if someone is maliciously trying to break our login flow.*/ throw new Error("Should never come here"); }
return oI.createCodePOST(input); }, }; }, }, }) ]})
#
Mark phone number as verified post OTP verificationThe OTP is verified in the {apiBasePath}/signinup/code/consume POST
API. Post verification, we want to change the access token payload to set the phone-verified
claim to true
. This can be done by overriding the consumeCodePOST
API:
import Passwordless from "supertokens-node/recipe/passwordless";import supertokens from "supertokens-node";import Session from "supertokens-node/recipe/session";
supertokens.init({ framework: "...", appInfo: { /*...*/ }, recipeList: [ Passwordless.init({ contactMethod: "PHONE", flowType: "USER_INPUT_CODE", override: { apis: (oI) => { return { ...oI, createCodePOST: async function (input) { if (oI.createCodePOST === undefined) { throw new Error("Should never come here"); }
/*...*/ /* Code from previous step... */ /*...*/ return oI.createCodePOST(input); }, consumeCodePOST: async function (input) { if (oI.consumeCodePOST === undefined) { throw new Error("Should never come here"); } // we should already have a session here since this is called // after phone password login let session = await Session.getSession(input.options.req, input.options.res, { overrideGlobalClaimValidators: () => [], }); if (session === undefined) { throw new Error("Should never come here"); }
// we add the session to the user context so that the createNewSession // function doesn't create a new session input.userContext.session = session; let resp = await oI.consumeCodePOST(input);
if (resp.status === "OK") { // OTP verification was successful. We can now mark the // session's payload as phone-verified: true so that // the user has access to API routes and the frontend UI await session.setClaimValue(PhoneVerifiedClaim, true, input.userContext); }
return resp; }, }; }, }, }) ]})
When we call the orginal implementation (await oI.consumeCodePOST(input);
), that function also calls the createNewSession
function from the session recipe. However, we already have a session from the first login challenge, so we must somehow communicate to the createNewSession
function to not create a new session in case a session already exists.
We do this by saving the current session in the input.userContext
object:
input.userContext.session = session;
Then in the createNewSession
function, we check if input.userContext.session
exists, and if it does, we do not create a new session, and instead just return the existing one:
import EmailPassword from "supertokens-node/recipe/emailpassword";import Passwordless from "supertokens-node/recipe/passwordless";import Session from "supertokens-node/recipe/session";import supertokens from "supertokens-node";
supertokens.init({ framework: "...", appInfo: { /*...*/ }, recipeList: [ EmailPassword.init({ /* ... */}), Passwordless.init({ /* ... */ }), Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, createNewSession: async function (input) { if (input.userContext.session !== undefined) { // if it comes here, it means that we already have an // existing session return input.userContext.session; } else { // we also get the phone number of the user and save it in the // session so that the OTP can be sent to it directly let userInfo = await EmailPassword.getUserById(input.userId, input.userContext); return originalImplementation.createNewSession({ ...input, accessTokenPayload: { ...input.accessTokenPayload, ...PhoneVerifiedClaim.build(input.userId, input.userContext), phoneNumber: userInfo?.email, }, }); } }, }; }, }, }) ]})