Skip to main content

Using a custom UI

caution
  • SuperTokens is not yet optimised for 2FA implementation, so you have to add a lot of customisations for it to work. We are working on improving the development experience for 2FA as well as adding more factors like TOPT. Stay tuned.
  • A demo app that uses the pre built UI can be found on our GitHub.

1) First factor recipe init#

Start by following the recipe guide for first factor login. To continue building our example app, we will use the thirdpartyemailpassword recipe as the first factor.

After following the frontend quick setup section, you should have the following supertokens.init:

import SuperTokens from 'supertokens-web-js';import Session from 'supertokens-web-js/recipe/session';import ThirdPartyEmailPassword from 'supertokens-web-js/recipe/thirdpartyemailpassword';
SuperTokens.init({    appInfo: {        apiDomain: "<YOUR_API_DOMAIN>",        apiBasePath: "/auth",        appName: "...",    },    recipeList: [        Session.init(),        ThirdPartyEmailPassword.init()    ],});

From here on, you can continue to build out the first factor's login form using the functions exposed from the supertokens-web-js SDK. See the thirdpartyemailpassword recipe guide for more information about the flows and functions.

2) Second factor recipe init#

For the second factor, we will be using the passwordless recipe. After following the frontend quick setup section, you should have the following supertokens.init:

import SuperTokens from 'supertokens-web-js';import Session from 'supertokens-web-js/recipe/session';import ThirdPartyEmailPassword from 'supertokens-web-js/recipe/thirdpartyemailpassword';import Passwordless from 'supertokens-web-js/recipe/passwordless';
SuperTokens.init({    appInfo: {        apiDomain: "<YOUR_API_DOMAIN>",        apiBasePath: "/auth",        appName: "...",    },    recipeList: [        Session.init(),        ThirdPartyEmailPassword.init(),        Passwordless.init(),    ],});

You can use the passwordless recipe function to build the second factor UI.

3) Reading 2FA completion information from the session for routing#

You will want to handle the routing of webapp to make sure that the correct login factor is being shown. This can be done by reading the session information.

Checking if the first factor login should be shown#

import Session from 'supertokens-web-js/recipe/session';
async function shouldShowFirstFactor() {    return !(await Session.doesSessionExist());}

If a session does not exist, this means that the user has not completed the first factor. In this case, you want to route them to the thirdpartyemailpassword login screen.

Checking if the second factor login should be shown#

import Session, { BooleanClaim } from 'supertokens-web-js/recipe/session';
export const SecondFactorClaim = new BooleanClaim({    id: "2fa-completed",    refresh: async () => {        // no-op    },});
async function shouldShowSecondFactor() {    if(await shouldShowFirstFactor()) {        return false;    }
    return !(await Session.getClaimValue({ claim: SecondFactorClaim }));}
async function shouldShowFirstFactor() {    return !(await Session.doesSessionExist());}
  • If a session does not exist, it means that the user has not finished the first factor yet.
  • If a session exists, but the SecondFactorClaim value is true, it means that the user has finished both the factors.
  • Otherwise the user has finished the first factor, but not the second one.

Protecting a website route that requires both the factors#

You can check if a user has finished both the login factors using the two functions above:

async function areBothLoginFactorsCompleted(): Promise<boolean> {    return !(await shouldShowFirstFactor()) && !(await shouldShowSecondFactor())}
areBothLoginFactorsCompleted().then(async (bothFactorsCompleted) => {    if (bothFactorsCompleted) {        // update state to show UI    } else {        if (await shouldShowFirstFactor()) {            // redirect user to first factor        } else {            // redirect user to second factor screen        }    }})

4) Getting the user's phone number for the second factor#

Once the user has finished the sign up process, we save their phone number in the session (as seen in the backend setup steps). This can be accessed on the frontend to send the OTP to the user without asking them to re-enter their phone after sign in:

import Session from 'supertokens-web-js/recipe/session';
async function getUsersPhoneNumber(): Promise<string | undefined> {    if (!(await Session.doesSessionExist())) {        // the user has not finished the first factor.        return undefined;    }    let accessTokenPayload = await Session.getAccessTokenPayloadSecurely();    if (accessTokenPayload.phoneNumber === undefined) {        // this means that the user is still signing up, or it means that the user        // had previously tried to sign up, but didn't complete the second factor step,        // and has now just signed in.
        // In this case, we should ask the user to enter their phone number.        return undefined;    }
    // An OTP can be sent to this phone for the second factor.     // No need to ask the user to enter their phone number again.     return accessTokenPayload.phoneNumber;}

5) Implementing logout#

If the user has completed both the factors, implementing the sign out feature can be done by:

import Session from 'supertokens-web-js/recipe/session';
async function signOut() {    await Session.signOut();    // redirect the user to the first factor login screen}

You should also implement a sign out button on the second factor screen, otherwise the user would be in a stuck state if they are unable to complete the second factor. To do this, you will need to call the signOut function as well as a function to clear the passwordless login state:

import Session from 'supertokens-web-js/recipe/session';import Passwordless from 'supertokens-web-js/recipe/passwordless';
async function signOut() {    if (await shouldShowSecondFactor()) {        // this means we are on the second factor screen now.        // calling the function below clears the login attempt info that is         // saved on the browser during passwordless login. This is needed so that        // future login attempts are not affected by the current one.        await Passwordless.clearLoginAttemptInfo();    }    await Session.signOut();    // redirect the user to the first factor login screen}