diff --git a/v2/change_me/ui-switcher.mdx b/v2/change_me/ui-switcher.mdx index c8c6770c6..96f71c067 100644 --- a/v2/change_me/ui-switcher.mdx +++ b/v2/change_me/ui-switcher.mdx @@ -9,6 +9,50 @@ show_ui_switcher: true import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" + + + + +## Pre build UI Last heading + + + + + + +TODO... React + + + + + +TODO... Angular + + + + + +TODO... Vue + + + + + + + + + + + +## Custom UI Last heading + + + + + + + + diff --git a/v2/community/reusableMD/mfa/MFAPaidBanner.mdx b/v2/community/reusableMD/mfa/MFAPaidBanner.mdx new file mode 100644 index 000000000..10de3ad68 --- /dev/null +++ b/v2/community/reusableMD/mfa/MFAPaidBanner.mdx @@ -0,0 +1,11 @@ +import CustomAdmonition from "/src/components/customAdmonition" + + + +This is a paid feature. + +For self hosted users, [Sign up](https://supertokens.com/auth) to get a license key and follow the instructions sent to you by email. Using the dev license key is free. We only start charging you once you enable the feature in production using the provided production license key. + +For managed service users, you can click on the "enable paid features" button on [our dashboard](https://supertokens.com/dashboard-saas), and follow the steps from there on. Once enabled, this feature is free on the provided development environment. + + diff --git a/v2/emailpassword/advanced-customizations/frontend-hooks/handle-event.mdx b/v2/emailpassword/advanced-customizations/frontend-hooks/handle-event.mdx index 84dba4210..4b15e0e78 100644 --- a/v2/emailpassword/advanced-customizations/frontend-hooks/handle-event.mdx +++ b/v2/emailpassword/advanced-customizations/frontend-hooks/handle-event.mdx @@ -27,11 +27,15 @@ EmailPassword.init({ // called when a user visits the login / sign up page with a valid session // in this case, they are usually redirected to the main app } else if (context.action === "SUCCESS") { - let user = context.user; - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // sign up success + if (context.createdNewSession) { + let user = context.user; + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // sign up success + } else { + // sign in success + } } else { - // sign in success + // this is during second factor login of step up auth flow } } } diff --git a/v2/emailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx b/v2/emailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx index 97418d5bd..5e79e74df 100644 --- a/v2/emailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx +++ b/v2/emailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx @@ -16,19 +16,28 @@ This function is used to change where the user is redirected to post certain act ```tsx import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; +import SuperTokens from "supertokens-auth-react"; -EmailPassword.init({ +SuperTokens.init({ + appInfo: { + appName: "SuperTokens", + apiDomain: "http://localhost:3000", + websiteDomain: "http://localhost:3000", + }, getRedirectionURL: async (context) => { - if (context.action === "RESET_PASSWORD") { - // called when the user clicked on the forgot password button - } else if (context.action === "SUCCESS") { - // called on a successful sign in / up. Where should the user go next? + if (context.action === "TO_AUTH") { + // This is called when we want to navigate to the login page. + // By default, this will go to the configured websiteBasePath (/auth) + } else if (context.action === "SUCCESS" && context.newSessionCreated) { + // This is called when the user has successfully signed in / signed up. + // By default, this will go to "/" or to + // the redirectToPath if it is set (the page from which the user was redirected to the auth page). let redirectToPath = context.redirectToPath; if (redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return redirectToPath; } - if (context.isNewPrimaryUser) { + if (context.createdNewUser) { // user signed up return "/onboarding" } else { @@ -38,7 +47,18 @@ EmailPassword.init({ } // return undefined to let the default behaviour play out return undefined; - } + }, + recipeList: [ + EmailPassword.init({ + getRedirectionURL: async (context) => { + if (context.action === "RESET_PASSWORD") { + // called when the user clicked on the forgot password button + } + // return undefined to let the default behaviour play out + return undefined; + } + }) + ] }); ``` diff --git a/v2/emailpassword/advanced-customizations/user-context.mdx b/v2/emailpassword/advanced-customizations/user-context.mdx index 532dda2fb..b7d637fb1 100644 --- a/v2/emailpassword/advanced-customizations/user-context.mdx +++ b/v2/emailpassword/advanced-customizations/user-context.mdx @@ -54,7 +54,7 @@ SuperTokens.init({ // override sign up using email / password signUp: async function (input) { let resp = await originalImplementation.signUp(input); - if (resp.status === "OK" && resp.user.loginMethods.length === 1) { + if (resp.status === "OK" && resp.user.loginMethods.length === 1 && input.session === undefined) { /* * This is called during the sign up API for email password login, * but before calling the createNewSession function. diff --git a/v2/emailpassword/common-customizations/account-linking/adding-accounts-to-session.mdx b/v2/emailpassword/common-customizations/account-linking/adding-accounts-to-session.mdx new file mode 100644 index 000000000..47608504a --- /dev/null +++ b/v2/emailpassword/common-customizations/account-linking/adding-accounts-to-session.mdx @@ -0,0 +1,191 @@ +--- +id: adding-accounts-to-session +title: Linking social accounts to an existing account +hide_title: true +--- + +import AccountLinkingPaidBanner from '../../../community/reusableMD/accountlinking/AccountLinkingPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Linking social accounts to an existing account + +There may be scenarios in which you want to link a social account to an existing user account. This guide will walk you through how to do this. + +The idea here is that we reuse the existing sign up APIs, but call them with a session's access token. The APIs will then create a new recipe user for that login method based on the input, and then link that to the session user. Of course, there are security checks done to ensure there is no account takeover risk, and we will go through them in this guide as well. + +:::caution +We do not provide pre built UIs for this flow since it's probably something you want to add in your settings page or during the sign up process, so this guide will focus on which APIs to call from your own UI. + +The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised our `supertokens-auth-react` SDK, on the frontend. +::: + +## Linking a social account to an existing user account + +### Step 1: Enable account linking and third party on the backend SDK + + + + +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +supertokens.init({ + supertokens: { + connectionURI: "...", + apiKey: "..." + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + ThirdParty.init({ /* ...*/ }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (user === undefined) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + if (session !== undefined && session.getUserId() === user.id) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + return { + shouldAutomaticallyLink: false + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +In the above implementation of `shouldDoAutomaticAccountLinking`, we only allow account linking if the input session is present. This means that we are trying to link a social login account to an existing session user. Otherwise, we do not allow account linking which means that first factor account linking is disabled. If you want to enable that too, you can see [this page](./automatic-account-linking). + +Read the [third party recipe docs](/docs/thirdparty/common-customizations/sign-in-and-up/built-in-providers#step-2-adding-providers-config-to-the-backend) to learn how to add provider config. + +### Step 2: Create a UI to show social login buttons and handle login + +First, you will need to detect which social login methods are already linked to the user. This can be done by inspecting the [user object](../../user-object) on the backend and checking the `thirdParty.id` property (the values will be like `google`, `facebook` etc). + +Then you will have to create your own UI which asks the user to pick a social login provider to connect to. Once they click on one, you will redirect them to that provider's page. Post login, the provider will redirect the user back to your application (on the same path as the first factor login) after which you will call our APIs to consume the OAuth tokens and link the user. + +The exact implementation of the above can be found [here](/docs/thirdparty/custom-ui/thirdparty-login). The two big differences in the implementation are: +- When you call the signinup API, you need to provide the session's access token in the request. If you are using our frontend SDK, this is done automatically via our frontend network interceptors. The access token will enable the backend to get a session and then link the social login account to session user. +- There are new types of failure scenarios when calling the signinup API which are not possible during first factor login. To learn more about them, see the [error codes section](/docs/thirdparty/common-customizations/account-linking/automatic-account-linking#err_code_001) (> `ERR_CODE_008`). + +### Step 3: Extract the social login access token and user peofile info on the backend + +Once you call the signinup API from the frontend, SuperTokens will verify the OAuth tokens and fetch the user's profile info from the third party provider. SuperTokens will also link the newly created recipe user to the session user. + +To fetch the new user object and also the third party profile, you can override the signinup recipe function: + + + + +```tsx +import SuperTokens, { User } from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // override the thirdparty sign in / up function + signInUp: async function (input) { + + let existingUser: User | undefined = undefined; + if (input.session !== undefined) { + existingUser = await SuperTokens.getUser(input.session.getUserId()); + } + + let response = await originalImplementation.signInUp(input); + + if (response.status === "OK") { + + let accessToken = response.oAuthTokens["access_token"]; + + let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; + + if (input.session !== undefined && response.user.id === input.session.getUserId()) { + if (response.user.loginMethods.length === existingUser!.loginMethods.length + 1) { + // new social account was linked to session user + } else { + // social account was already linked to the session + // user from before + } + } + } + + return response; + } + } + } + } + // highlight-end + }), + Session.init({ /* ... */ }) + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +Notice in the above snippet that we check for `input.session !== undefined && response.user.id === input.session.getUserId()`. This ensures that we run our custom logic only if it's linking a social account to your session account, and not during first factor login. \ No newline at end of file diff --git a/v2/emailpassword/common-customizations/change-password.mdx b/v2/emailpassword/common-customizations/change-password.mdx index f676928f8..402d717be 100644 --- a/v2/emailpassword/common-customizations/change-password.mdx +++ b/v2/emailpassword/common-customizations/change-password.mdx @@ -137,7 +137,7 @@ app.post("/change-password", verifySession(), async (req: SessionRequest, res: e const email = loginMethod.email!; // call signin to check that input password is correct - let isPasswordValid = await ^{recipeNameCapitalLetters}.^{nodeSignIn}(session!.getTenantId(), email, oldPassword) + let isPasswordValid = await ^{recipeNameCapitalLetters}.^{nodeVerifyCredentials}(session!.getTenantId(), email, oldPassword) if (isPasswordValid.status !== "OK") { // TODO: handle incorrect password error diff --git a/v2/emailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx b/v2/emailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx index 8e9399520..2a898c360 100644 --- a/v2/emailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx +++ b/v2/emailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx @@ -460,25 +460,25 @@ const FAKE_PASSWORD = "asokdA87fnf30efjoiOI**cwjkn"; export function POST(request: NextRequest) { return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - const body = await request.json(); - let email = body.email; + if (err) { + return NextResponse.json(err, { status: 500 }); + } + const body = await request.json(); + let email = body.email; - let signUpResult = await ^{recipeNameCapitalLetters}.^{webjsEmailPasswordSignUp}("public", email, FAKE_PASSWORD); - if (signUpResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return NextResponse.json({ message: 'User already exists' }, { status: 400 }); - } + let signUpResult = await ^{recipeNameCapitalLetters}.^{webjsEmailPasswordSignUp}("public", email, FAKE_PASSWORD); + if (signUpResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { + return NextResponse.json({ message: 'User already exists' }, { status: 400 }); + } - // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); + // we successfully created the user. Now we should send them their invite link + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); - return NextResponse.json({ message: 'Success' }) + return NextResponse.json({ message: 'Success' }) }, { - overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] - } + overrideGlobalClaimValidators: async function (globalClaimValidators) { + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] + } }); } ``` @@ -510,8 +510,8 @@ export class CreateUserController { let signUpResult = await ^{recipeNameCapitalLetters}.^{webjsEmailPasswordSignUp}("public", email, FAKE_PASSWORD); if (signUpResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { - // TODO: send 400 response to the client. - return; + // TODO: send 400 response to the client. + return; } // we successfully created the user. Now we should send them their invite link diff --git a/v2/emailpassword/common-customizations/embed-sign-in-up-form.mdx b/v2/emailpassword/common-customizations/embed-sign-in-up-form.mdx index 77b944af4..edef87985 100644 --- a/v2/emailpassword/common-customizations/embed-sign-in-up-form.mdx +++ b/v2/emailpassword/common-customizations/embed-sign-in-up-form.mdx @@ -49,31 +49,25 @@ SuperTokens.init({ appName: "...", websiteDomain: "..." }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "TO_AUTH") { + return "/auth"; // return the path where you are rendering the Auth UI + } else if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + }; + }, + // highlight-end recipeList: [ EmailPassword.init({ signInAndUpFeature: { // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], - // highlight-start - async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { - return "/auth"; // return the path where you are rendering the Auth UI - }; - }, - // highlight-end }); function AuthPage() { @@ -112,31 +106,25 @@ SuperTokens.init({ appName: "...", websiteDomain: "..." }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "TO_AUTH") { + return "/auth"; // return the path where you are rendering the Auth UI + } else if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + }; + }, + // highlight-end recipeList: [ EmailPassword.init({ signInAndUpFeature: { // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], - // highlight-start - async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { - return "/auth"; // return the path where you are rendering the Auth UI - }; - }, - // highlight-end }); function AuthPage() { @@ -177,31 +165,25 @@ SuperTokens.init({ appName: "...", websiteDomain: "..." }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "TO_AUTH") { + return "/auth"; // return the path where you are rendering the Auth UI + } else if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + }; + }, + // highlight-end recipeList: [ EmailPassword.init({ signInAndUpFeature: { // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], - // highlight-start - async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { - return "/auth"; // return the path where you are rendering the Auth UI - }; - }, - // highlight-end }); function AuthPage() { @@ -223,8 +205,7 @@ function AuthPage() { In the above code snippet, we: 1. Disabled the default Auth UI by setting `disableDefaultUI` to `true` inside the EmailPassword recipe config. -2. Overrode the `getRedirectionURL` function inside the EmailPassword recipe config to redirect to `/dashboard` upon successful login. -3. Overrode the `getRedirectionURL` function inside the SuperTokens config to redirect to `/auth` when login is required. +2. Override the `getRedirectionURL` function inside the SuperTokens config to redirect to `/auth` when login is required and to redirect to `/dashboard` upon successful login. Feel free to customize the redirection URLs as needed. diff --git a/v2/emailpassword/common-customizations/handling-signup-success.mdx b/v2/emailpassword/common-customizations/handling-signup-success.mdx index f01699989..77ab6cd2f 100644 --- a/v2/emailpassword/common-customizations/handling-signup-success.mdx +++ b/v2/emailpassword/common-customizations/handling-signup-success.mdx @@ -116,7 +116,7 @@ SuperTokens.init({ let response = await originalImplementation.signUp(input); // Post sign up response, we check if it was successful - if (response.status === "OK" && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { /** * * response.user contains the following info: @@ -293,7 +293,7 @@ SuperTokens.init({ let response = await originalImplementation.signUpPOST!(input); // Post sign up response, we check if it was successful - if (response.status === "OK" && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { /** * * response.user contains the following info: diff --git a/v2/emailpassword/common-customizations/multi-tenancy/common-domain-login.mdx b/v2/emailpassword/common-customizations/multi-tenancy/common-domain-login.mdx index 6d8a8d318..3c997378b 100644 --- a/v2/emailpassword/common-customizations/multi-tenancy/common-domain-login.mdx +++ b/v2/emailpassword/common-customizations/multi-tenancy/common-domain-login.mdx @@ -285,7 +285,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.emailPassword.enabled) { + if (loginMethods.firstFactors.includes("emailpassword")) { // email password login is enabled for the tenant } else { // check for other login methods being enabled for the tenant. @@ -303,7 +303,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.emailPassword.enabled) { + if (loginMethods.firstFactors.includes("emailpassword")) { // email password login is enabled for the tenant } else { // check for other login methods being enabled for the tenant. @@ -648,23 +648,25 @@ SuperTokens.init({ appName: "...", websiteDomain: "...", }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + let claimValue: string[] | undefined = await Session.getClaimValue({ + claim: Multitenancy.AllowedDomainsClaim + }); + if (claimValue !== undefined) { + window.location.href = "https://" + claimValue[0]; + } else { + // there was no configured allowed domain for this user. Throw an error cause of + // misconfig or redirect to a default sub domain + } + } + return undefined; + }, + // highlight-end recipeList: [ ^{recipeNameCapitalLetters}.init({ ^{reactRecipeInitDefault} - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { - let claimValue: string[] | undefined = await Session.getClaimValue({ - claim: Multitenancy.AllowedDomainsClaim - }); - if (claimValue !== undefined) { - window.location.href = "https://" + claimValue[0]; - } else { - // there was no configured allowed domain for this user. Throw an error cause of - // misconfig or redirect to a default sub domain - } - } - return undefined; - } }), ] }); diff --git a/v2/emailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx b/v2/emailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx index bbca9ff7f..a978a615f 100644 --- a/v2/emailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx +++ b/v2/emailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx @@ -159,7 +159,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.emailPassword.enabled) { + if (loginMethods.firstFactors.includes("emailpassword")) { // email password login is enabled for the tenant } else { // check for other login methods being enabled for the tenant. @@ -177,7 +177,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.emailPassword.enabled) { + if (loginMethods.firstFactors.includes("emailpassword")) { // email password login is enabled for the tenant } else { // check for other login methods being enabled for the tenant. diff --git a/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx b/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx index 8900a22f7..545e51ca3 100644 --- a/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/emailpassword/common-customizations/sessions/claims/claim-validators.mdx @@ -52,17 +52,20 @@ Without a special construct of session claim validators, the updating of the ses Before we dive deep into claim validators, let's talk about session claim objects. These are objects that conform to an interface that allows SuperTokens to automatically add session claims to the access token payload. Here is the interface: ```tsx + +import { RecipeUserId } from "supertokens-node"; + type JSONObject = any; // typecheck-only, removed from output interface SessionClaim { readonly key: string; - fetchValue(userId: string, tenantId: string, userContext: any): Promise; + fetchValue(userId: string, recipeUserId: RecipeUserId, tenantId: string, currentPayload: JSONObject | undefined, userContext: any): Promise; addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject; - removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayloadByMerge_internal(payload: JSONObject, userContext: any): JSONObject; - removeFromPayload(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayload(payload: JSONObject, userContext: any): JSONObject; getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; @@ -364,7 +367,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)) }; /* @@ -509,8 +512,8 @@ The `build` function is a helper function which all claims have that does the fo ```text class Claim { // other functions like fetchValue, getValueFromPayload etc.. - function build(userId, tenantId) { - claimValue = this.fetchValue(userId, tenantId); + function build(userId, recipeUserId, tenantId) { + claimValue = this.fetchValue(userId, recipeUserId, tenantId); return this.addToPayload_internal({}, claimValue) } } diff --git a/v2/emailpassword/common-customizations/sessions/fetch-sessions-for-user.mdx b/v2/emailpassword/common-customizations/sessions/fetch-sessions-for-user.mdx index 989d8c1cb..cfec26595 100644 --- a/v2/emailpassword/common-customizations/sessions/fetch-sessions-for-user.mdx +++ b/v2/emailpassword/common-customizations/sessions/fetch-sessions-for-user.mdx @@ -121,6 +121,6 @@ for session_handle in session_handles: -By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to delete the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. +By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to fetch the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. diff --git a/v2/emailpassword/common-customizations/sessions/jwt-signing-key-rotation.mdx b/v2/emailpassword/common-customizations/sessions/jwt-signing-key-rotation.mdx index 097aeeaa9..b4ab73477 100644 --- a/v2/emailpassword/common-customizations/sessions/jwt-signing-key-rotation.mdx +++ b/v2/emailpassword/common-customizations/sessions/jwt-signing-key-rotation.mdx @@ -90,6 +90,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -114,6 +119,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -134,9 +152,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -147,7 +162,10 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + diff --git a/v2/emailpassword/common-customizations/sessions/revoke-session.mdx b/v2/emailpassword/common-customizations/sessions/revoke-session.mdx index 3b7acdf47..c474e8976 100644 --- a/v2/emailpassword/common-customizations/sessions/revoke-session.mdx +++ b/v2/emailpassword/common-customizations/sessions/revoke-session.mdx @@ -46,7 +46,7 @@ function NavBar() { async function onLogout() { // highlight-next-line await signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } return (
    @@ -71,7 +71,7 @@ import Session from "supertokens-auth-react/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -101,7 +101,7 @@ import Session from "supertokens-web-js/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -113,7 +113,7 @@ import supertokensSession from "supertokens-web-js-script/recipe/session"; async function logout () { // highlight-next-line await supertokensSession.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` diff --git a/v2/emailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/emailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx index c986e3b97..d1f0e905a 100644 --- a/v2/emailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/emailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -170,6 +170,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -194,6 +199,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -214,9 +232,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -227,9 +242,12 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + diff --git a/v2/emailpassword/common-customizations/userid-format.mdx b/v2/emailpassword/common-customizations/userid-format.mdx index 7abbf32e3..dd82c36c7 100644 --- a/v2/emailpassword/common-customizations/userid-format.mdx +++ b/v2/emailpassword/common-customizations/userid-format.mdx @@ -54,7 +54,7 @@ SuperTokens.init({ signUp: async function (input) { let response = await originalImplementation.signUp(input); - if (response.status === "OK" && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { // highlight-start let externalUserId = "" await SuperTokens.createUserIdMapping({ superTokensUserId: response.user.id, externalUserId }) diff --git a/v2/emailpassword/custom-ui/email-password-login.mdx b/v2/emailpassword/custom-ui/email-password-login.mdx index 5adccfd9f..121696715 100644 --- a/v2/emailpassword/custom-ui/email-password-login.mdx +++ b/v2/emailpassword/custom-ui/email-password-login.mdx @@ -54,8 +54,10 @@ async function signUpClicked(email: string, password: string) { } }) } else if (response.status === "SIGN_UP_NOT_ALLOWED") { - // this can happen during automatic account linking. Tell the user to use another - // login method, or go through the password reset flow. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign up was not allowed. + window.alert(response.reason) } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. @@ -103,8 +105,10 @@ async function signUpClicked(email: string, password: string) { } }) } else if (response.status === "SIGN_UP_NOT_ALLOWED") { - // this can happen during automatic account linking. Tell the user to use another - // login method, or go through the password reset flow. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in was not allowed. + window.alert(response.reason) } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. @@ -157,7 +161,7 @@ The response body from the API call has a `status` property in it: Either way, you want to show the user an error next to the input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking. Tell the user to use another login method, or go through the password reset flow. +- `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign up was not allowed. @@ -303,8 +307,10 @@ async function signInClicked(email: string, password: string) { } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect.") } else if (response.status === "SIGN_IN_NOT_ALLOWED") { - // this can happen due to automatic account linking. Tell the user that their - // input credentials is wrong (so that they do through the password reset flow) + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in was not allowed. + window.alert(response.reason) } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. @@ -349,8 +355,10 @@ async function signInClicked(email: string, password: string) { } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect.") } else if (response.status === "SIGN_IN_NOT_ALLOWED") { - // this can happen due to automatic account linking. Tell the user that their - // input credentials is wrong (so that they do through the password reset flow) + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in was not allowed. + window.alert(response.reason) } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. @@ -400,7 +408,7 @@ The response body from the API call has a `status` property in it: - `status: "WRONG_CREDENTIALS_ERROR"`: The input email and password combination is incorrect. - `status: "FIELD_ERROR"`: This indicates that the input email did not pass the backend validation - probably because it's syntactically not an email. You want to show the user an error next to the email input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking. Tell the user that their input credentials is wrong (so that they do through the password reset flow). +- `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in was not allowed. diff --git a/v2/emailpassword/custom-ui/sign-out.mdx b/v2/emailpassword/custom-ui/sign-out.mdx index c851395f9..af9169fa0 100644 --- a/v2/emailpassword/custom-ui/sign-out.mdx +++ b/v2/emailpassword/custom-ui/sign-out.mdx @@ -28,7 +28,7 @@ import Session from "supertokens-web-js/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` @@ -40,7 +40,7 @@ import supertokensSession from "supertokens-web-js-script/recipe/session"; async function logout () { // highlight-next-line await supertokensSession.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` diff --git a/v2/emailpassword/mfa.mdx b/v2/emailpassword/mfa.mdx new file mode 100644 index 000000000..1c72b293a --- /dev/null +++ b/v2/emailpassword/mfa.mdx @@ -0,0 +1,9 @@ +--- +id: mfa +title: Multi factor auth +hide_title: true +--- + +# Multi factor auth + +See our guide for Multi Factor Auth [here](/docs/mfa/introduction). \ No newline at end of file diff --git a/v2/emailpassword/migration/about.mdx b/v2/emailpassword/migration/about.mdx index ed9b1b976..08b0ef756 100644 --- a/v2/emailpassword/migration/about.mdx +++ b/v2/emailpassword/migration/about.mdx @@ -45,3 +45,6 @@ There are 3 steps to user migration: - This will prevent users from having to re-authenticate. You can learn more about how to implement these changes in the [Session Migration](./session-migration) section. + +## Step 4) MFA migration +If you are using MFA in your app, checkout the MFA migration section [here](/docs/mfa/migration) after you have gone through the above migration steps. diff --git a/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx b/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx index b3e8ddd0b..0b9238935 100644 --- a/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx +++ b/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx @@ -211,7 +211,7 @@ EmailPassword.init({ } // Call the signup function to create a new SuperTokens user. - let signUpResponse = await EmailPassword.signUp(input.email, input.password, input.userContext) + let signUpResponse = await EmailPassword.signUp(input.tenantId, input.email, input.password, undefined, input.userContext) if (signUpResponse.status !== "OK") { throw new Error("Should never come here") } @@ -229,7 +229,7 @@ EmailPassword.init({ if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email - await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, input.userContext); + await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); } } @@ -493,7 +493,7 @@ EmailPassword.init({ if (legacyUserInfo) { // create a SuperTokens account for the user with a temporary password let tempPassword = await generatePassword(); - let signUpResponse = await EmailPassword.signUp(email, tempPassword, input.userContext); + let signUpResponse = await EmailPassword.signUp(input.tenantId, email, tempPassword, undefined, input.userContext); if (signUpResponse.status === "OK") { @@ -510,7 +510,7 @@ EmailPassword.init({ if (generateEmailVerificationTokenResponse.status === "OK") { // verify the user's email - await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, input.userContext); + await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); } } @@ -975,7 +975,7 @@ EmailPassword.init({ } } // Call the signup function to create a new SuperTokens user. - let signUpResponse = await EmailPassword.signUp(input.email, input.password, input.userContext) + let signUpResponse = await EmailPassword.signUp(input.tenantId, input.email, input.password, undefined, input.userContext) if (signUpResponse.status !== "OK") { throw new Error("Should never come here") } @@ -993,7 +993,7 @@ EmailPassword.init({ if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email - await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, input.userContext); + await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); } } diff --git a/v2/emailpassword/migration/mfa-migration.mdx b/v2/emailpassword/migration/mfa-migration.mdx new file mode 100644 index 000000000..752752ef3 --- /dev/null +++ b/v2/emailpassword/migration/mfa-migration.mdx @@ -0,0 +1,12 @@ +--- +id: mfa-migration +title: Step 4) MFA migration +hide_title: true +--- + + + + +# MFA migration + +If you are using MFA in your app, checkout the MFA migration section [here](/docs/mfa/migration) after you have gone through the previous steps in migration. \ No newline at end of file diff --git a/v2/emailpassword/multi-tenant.mdx b/v2/emailpassword/multi-tenant.mdx new file mode 100644 index 000000000..42d32e856 --- /dev/null +++ b/v2/emailpassword/multi-tenant.mdx @@ -0,0 +1,9 @@ +--- +id: multi-tenant +title: Multi tenancy / B2B orgs +hide_title: true +--- + +# Multi tenancy / B2B orgs + +See our guide for multi tenancy auth [here](/docs/multitenancy/introduction). \ No newline at end of file diff --git a/v2/emailpassword/nextjs/app-directory/setting-up-frontend.mdx b/v2/emailpassword/nextjs/app-directory/setting-up-frontend.mdx index 31028dab7..68c551eac 100644 --- a/v2/emailpassword/nextjs/app-directory/setting-up-frontend.mdx +++ b/v2/emailpassword/nextjs/app-directory/setting-up-frontend.mdx @@ -62,7 +62,9 @@ export default function Auth() { ## 3) Visit `/auth` page on your website -If you see a login UI, then you have successfully completed this step! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) +If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre built UI, for each page on [this link](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/auth-page--playground). + +If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/v2/emailpassword/nextjs/setting-up-frontend.mdx b/v2/emailpassword/nextjs/setting-up-frontend.mdx index 9f7cf1295..e1f0a9e94 100644 --- a/v2/emailpassword/nextjs/setting-up-frontend.mdx +++ b/v2/emailpassword/nextjs/setting-up-frontend.mdx @@ -59,7 +59,9 @@ export default function Auth() { ## 3) Visit `/auth` page on your website -If you see a login UI, then you have successfully completed this step! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) +If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre built UI, for each page on [this link](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/auth-page--playground). + +If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/v2/emailpassword/pre-built-ui/auth-redirection.mdx b/v2/emailpassword/pre-built-ui/auth-redirection.mdx index 72a286829..9cc746323 100644 --- a/v2/emailpassword/pre-built-ui/auth-redirection.mdx +++ b/v2/emailpassword/pre-built-ui/auth-redirection.mdx @@ -30,26 +30,26 @@ SuperTokens.init({ appName: "...", websiteDomain: "...", }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + if (context.redirectToPath !== undefined) { + // we are navigating back to where the user was before they authenticated + return context.redirectToPath; + } + if (context.createdNewUser) { + // user signed up + } else { + // user signed in + } + return "/dashboard"; + } + return undefined; + }, + // highlight-end recipeList: [ ^{recipeNameCapitalLetters}.init({ ^{reactRecipeInitDefault} // typecheck-only, removed from output - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { - if (context.redirectToPath !== undefined) { - // we are navigating back to where the user was before they authenticated - return context.redirectToPath; - } - if (context.isNewPrimaryUser) { - // user signed up - } else { - // user signed in - } - return "/dashboard"; - } - return undefined; - } - // highlight-end }), ] }); diff --git a/v2/emailpassword/pre-built-ui/sign-out.mdx b/v2/emailpassword/pre-built-ui/sign-out.mdx index 6cf670151..ed1a43dca 100644 --- a/v2/emailpassword/pre-built-ui/sign-out.mdx +++ b/v2/emailpassword/pre-built-ui/sign-out.mdx @@ -26,7 +26,7 @@ function NavBar() { async function onLogout() { // highlight-next-line await signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } return (
      @@ -51,7 +51,7 @@ import Session from "supertokens-auth-react/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` diff --git a/v2/emailpassword/sidebars.js b/v2/emailpassword/sidebars.js index e7964f267..4d4fd559e 100644 --- a/v2/emailpassword/sidebars.js +++ b/v2/emailpassword/sidebars.js @@ -462,6 +462,13 @@ module.exports = { "common-customizations/get-user-info", "common-customizations/user-pagination", "common-customizations/delete-user", + { + type: "category", + label: "Account Linking", + items: [ + "common-customizations/account-linking/adding-accounts-to-session" + ] + }, "common-customizations/change-password", "common-customizations/change-email-post-login", { @@ -674,7 +681,9 @@ module.exports = { "user-roles/get-all-roles", "user-roles/delete-roles", ], - } + }, + "mfa", + "multi-tenant" ] }, "rate-limits", @@ -703,7 +712,8 @@ module.exports = { ], }, "migration/data-migration", - "migration/session-migration" + "migration/session-migration", + "migration/mfa-migration" ], }, { diff --git a/v2/emailpassword/supabase-intergration/backend-signup-override.mdx b/v2/emailpassword/supabase-intergration/backend-signup-override.mdx index 344a1363e..3007dfb4b 100644 --- a/v2/emailpassword/supabase-intergration/backend-signup-override.mdx +++ b/v2/emailpassword/supabase-intergration/backend-signup-override.mdx @@ -50,7 +50,7 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.signUpPOST(input); - if (response.status === "OK" && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); diff --git a/v2/mfa/backend-setup.mdx b/v2/mfa/backend-setup.mdx new file mode 100644 index 000000000..2db15c01f --- /dev/null +++ b/v2/mfa/backend-setup.mdx @@ -0,0 +1,914 @@ +--- +id: backend-setup +title: Backend setup +hide_title: true +--- + +import MFAPaidBanner from '../community/reusableMD/mfa/MFAPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; +import TabItem from '@theme/TabItem'; +import PythonSyncAsyncSubTabs from "/src/components/tabs/PythonSyncAsyncSubTabs"; +import CoreInjector from "/src/components/coreInjector" +import CustomAdmonition from "/src/components/customAdmonition" + + + +# Backend setup + +## Step 1: Enable account linking +MFA requires account linking to be enabled (see [here](./important-concepts#relation-of-account-linking-and-mfa) to understand why). You can enable it in the following way: + + + + +```ts +import SuperTokens, { User, RecipeUserId, } from "supertokens-node"; +import { UserContext } from "supertokens-node/types"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import Passwordless from "supertokens-node/recipe/passwordless" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import totp from "supertokens-node/recipe/totp" +import AccountLinking from "supertokens-node/recipe/accountlinking" +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +SuperTokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + // ... + // highlight-start + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { + // This will enable first factor account linking. + // For example, if a user logs in via email password with email e1, + // and then signs out and logs in via Google with the same email, + // we will link the accounts (as long as the email password user's email is + // verified). + // It will also enable account linking for MFA purposes. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + }), + // highlight-end + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +The above snippet enables account linking for first factor login, and also for MFA purposes. However, if you want to enable account linking only for MFA purposes, you can see [this section](#enabling-account-linking-only-for-mfa-purposes). + +:::important +Account linking is a paid feature, and you need to generate a license key to enable it. Enabling the MFA feature also enables account linking automatically, so you don't need to check the account linking feature separately. +::: + +## Step 2: Configuring the first factors + +### Single tenant setup + +We start by intialising the MFA recipe on the backend and specifying the list of first factors using their [factor IDs](./important-concepts#auth-factor-ids). You still have to initialise all the auth recipes in the `recipeList`, and configure them based on your needs. + +For example, the code below inits `thirdpartyemailpassword` and `passwordless` recipes and sets the `firstFactor` array to be `["emailpassword", "thirdparty"]`. This means that we will show email password and social login to the user as the first factor (using the `thirdpartyemailpassword` recipe), and use `passwordless` for the second factor. + + + + +```ts +import supertokens from "supertokens-node"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import Passwordless from "supertokens-node/recipe/passwordless" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + // ... + // highlight-start + ThirdPartyEmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE" + }), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"] + }) + // highlight-end + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +There are of course other combinations of first factors that you may want to add. For example, if you want passwordless as the first factor, then you would init the passwordless recipe and add `"passwordless"` in the `firstFactors` array. + +### Multi tenant setup + +For a multi tenancy setup, where each tenant can have a different set of first factors, you can leave the `firstFactors` array as `undefined` in the `MultiFactorAuth.init` and configure the firstFactors on a per tenant basis when you are creating / updating a tenant as shown below: + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function createNewTenant() { + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + firstFactors: ["emailpassword"] + }); + + if (resp.createdNew) { + // Tenant created successfully + } else { + // Existing tenant's config was modified. + } +} +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + + + + +```bash +curl --location --request PUT '^{coreInjector_uri_without_quotes}/appid-/recipe/multitenancy/tenant' \ +--header 'api-key: ^{coreInjector_api_key_without_quotes}' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "tenantId": "customer1", + "emailPasswordEnabled": true, + "passwordlessEnabled": true, + "firstFactors": ["emailpassword"] +}' +``` + + + + + + + +In the above, we are enabling email password and passwordless for the tenant, however, we have set `firstFactors` only to include `"emailpassword"`. This means that users who login to this tenant will only be able to use email password as the first factor, even though passwordless is enabled. Later on, we will see how we can configure passwordless as a second factor for this tenant. + +:::important +- If you do not configure a `firstFactors` array on a tenant config, then it will pick up the values from the `firstFactors` array in the `MultiFactorAuth.init` from the backend's `init` config. +- To remove the `firstFactors` configuation for a tenant, you can simply pass a `null` value for the `firstFactors` key in the tenant config. For that tenant, this will make SuperTokens default to the `firstFactors` array in the `MultiFactorAuth.init` from the backend's `init` config. +::: + +## Step 3: Configuring a second factor + +In this section, we will see how to configure SuperTokens so that a second factor is required for all users during sign up and during sign in. We will use TOTP as an example for the second factor. + +### Single tenant setup +This can be achieved easily by the following code snippet: + + + + +```ts +import SuperTokens, { User, RecipeUserId, } from "supertokens-node"; +import { UserContext } from "supertokens-node/types"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import Passwordless from "supertokens-node/recipe/passwordless" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import totp from "supertokens-node/recipe/totp" +import AccountLinking from "supertokens-node/recipe/accountlinking" +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + + +SuperTokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + // ... + ThirdPartyEmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE" + }), + totp.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + return ["totp"] + } + } + } + } + // highlight-end + }) + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +In the above snippet, we have configured email password and social login as the first factor, followed by totp as the second factor. + +After sign in or sign up, SuperTokens calls the `getMFARequirementsForAuth` function to get a list of secondary factors for the user. The returned value is used to determine the boolean value of `v` that's stored in the session's access token payload. If the returned factor is already completed (it's in the `c` object of the session's payload), then the value of `v` will be `true`, else `false`. + +In the above example, we are simply returning `"totp"` as a required factor for all users, but you can also dynamically decide which factor to return based on the `input` arguments, which contains the `User` object, the `tenantId` and the current session's access token payload. In fact, the default implementation of `getMFARequirementsForAuth` returns the set of factors that are specifically enabled for this user (see next section) or for the tenant (see later section). + +The output of this function can be more complex than just a `string[]`. You can also return an object which tells SuperTokens that any one of the factors need to be completed: + + + + +```ts +import supertokens from "supertokens-node"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + // ... + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + // highlight-start + return [{ + oneOf: ["totp", "otp-email"] + }] + // highlight-end + } + } + } + } + }) + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +Or that all of the factors in the returned array need to be completed: + + + + +```ts +import supertokens from "supertokens-node"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + // ... + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + // highlight-start + return [{ + allOfInAnyOrder: ["totp", "otp-email"] + }] + // highlight-end + } + } + } + } + }) + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +In the above, the user will have to complete both the factors in any order. + +You can enforce order of factors in the auth flow by returning different values from the function based on what's already completed. For example, if you want to have email password / social login as the first factor followed by TOTP and then email otp-email (in that order), you could do the following: + + + + +```ts +import supertokens from "supertokens-node"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + // ... + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + // highlight-start + let currentCompletedFactors = MultiFactorAuth.MultiFactorAuthClaim.getValueFromPayload(input.accessTokenPayload) + if ("totp" in currentCompletedFactors) { + // this means the totp factor is completed + return ["otp-email"] + } else { + // this means we have not finished totp yet, and we want + // to do that right after first factor login + return ["totp"] + } + // highlight-end + } + } + } + } + }) + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +You can return an empty array from `getMFARequirementsForAuth` if you don't want any further MFA done for the current user. + +If you return more than one item from the array, it would mean that the user has to satisfy the criteria for each of the items. For example, if we returnt the following from `getMFARequirementsForAuth`: + +```ts +let requirements = [{ + oneOf: ["f1", "f2"] +}, { + allOfInAnyOrder: ["f3", "f4"] +}, "f5"] +``` + +Then the user will have to complete (`f1` or `f2`) and `f3`, `f4`, `f5` to complete login, **in that order**. Note that `f3` and `f4` can be done in any order, but they both have to be done before doing `f5`. + +All of the above is only useful to populate the values of the [`v` and `c` values](./important-concepts#how-are-auth-factors-marked-as-completed) in the session's access token payload. However, you still need to [protect the frontend and API routes](./protect-routes) to ensure that those resources are given access to only if the user has the `v` boolean set to `true` (indicating that all factors have been completed). + + +### Multi tenant setup +For a multi tenant setup, you can configure a list of secondary factors when creating / modifying a tenant as shown below: + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function createNewTenant() { + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + emailPasswordEnabled: true, + passwordlessEnabled: true, + firstFactors: ["emailpassword"], + requiredSecondaryFactors: ["otp-email"] + }); + + if (resp.createdNew) { + // Tenant created successfully + } else { + // Existing tenant's config was modified. + } +} +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + + + + +```bash +curl --location --request PUT '^{coreInjector_uri_without_quotes}/appid-/recipe/multitenancy/tenant' \ +--header 'api-key: ^{coreInjector_api_key_without_quotes}' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "tenantId": "customer1", + "emailPasswordEnabled": true, + "passwordlessEnabled": true, + "firstFactors": ["emailpassword"], + "requiredSecondaryFactors": ["otp-email"] +}' +``` + + + + + + + +In the above code, we add a propery called `requiredSecondaryFactors` for a tenant whose value is a `string[]`. We add `otp-email` as a factor ID above which means that all users who log into that tenant must complete `otp-email` as a second factor. This factor comes from the passwordless recipe, and so we have also set `passwordlessEnabled: true`. + +In order to remove the `requiredSecondaryFactors` configuration for a tenant, you can simply pass a `null` value for the `requiredSecondaryFactors` key in the tenant config. + +If you add more than one item in this array, it means that the user must complete any one of factors mentioned in the array. If you want to have a different behaviour for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: + + + + +```ts +import supertokens from "supertokens-node"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + // ... + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + // highlight-start + return [{ + allOfInAnyOrder: await input.requiredSecondaryFactorsForTenant + }] + // highlight-end + } + } + } + } + }) + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +Notice that the input to the function contains the `requiredSecondaryFactorsForTenant` array. This would be the same list that you passed to the tenant config when creating / modifying the tenant as shown in the previous steps. + +### Functions to help with configuring a second factor for each user optionally + +Instead of configuring a factor for all users in your app, or for all users within a tenant, you may want to implement a flow in which users do MFA only if they have enabled it for themselves. Here, users may also want to choose what factors they would like to enable for themselves. + +This flow is usually achieved by allowing users to configure their MFA preferences in the settings page in your app's frontend. We don't yet provide a pre built UI for this, but in this section, we will talk about how to setup this up on the backend. + +You want to start by creating an API that does [session verification](/docs/session/common-customizations/sessions/session-verification-in-api/overview), and then enable the desired factor for the user. For example, if the user wants to enable TOTP, then you would call the following function in your API: + + + + + + + + +```ts +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +async function enableMFAForUser(userId: string) { + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, "totp") +} +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + + + + +The effect of the above function call is that in the default implementation of `getMFARequirementsForAuth`, we take into account the factors that are specifically enabled for the input user. By default, if you add several factors for a user ID, then it would require them to complete any one of those secondary factors during login. + +If you want to change the default behaviour from "any one of" to something else (like "all of"), you can do so by overriding the `getMFARequirementsForAuth` function: + + + + +```ts +import supertokens from "supertokens-node"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + // ... + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // highlight-start + getMFARequirementsForAuth: async function (input) { + return [{ + allOfInAnyOrder: await input.requiredSecondaryFactorsForUser + }] + } + // highlight-end + } + } + } + }) + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +Once you call the `addToRequiredSecondaryFactorsForUser` function for a user, SuperTokens will save this preference in the usermetadata JSON of the user. For example, if you add `"totp"` as a requred secondary factor for a user, this will be saved in the metadata JSON as: + +```json +{ + "_supertokens": { + "requiredSecondaryFactors": ["totp"] + } +} +``` + +You can view this JSON on the [user details page of the user management dashboard](/docs/userdashboard/users-listing-and-details) and modify it manually if you like. + +In order to know the factors that have been enabled for a user, you can use the following function: + + + + + + + + +```ts +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +async function isTotpEnabledForUser(userId: string) { + let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId) + return factors.includes("totp") +} +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + + + +Using the above function, you can build your settings page on the frontend which displays the existing enabled factors for the user, and allow users to enable / disable factors as they like. + +Once you have enabled a factor for a user, you take them to that factor setup screen if they have not previously already setup the factor. To know if a factor is setup, you can call the following function (on the backend): + + + + +```ts +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +async function isTotpSetupForUser(userId: string) { + let factors = await MultiFactorAuth.getFactorsSetupForUser(userId) + return factors.includes("totp") +} +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +Or you can call the [MFAInfo endpoint](./frontend-setup#mfa-info-endpoint) from the frontend which returns information indicating which factors have already been setup for the user and which not. + +A factor is considered setup if the user has gone through that factor's flow at least once. For example, if the user has created and verified a TOTP device, only then will the `getFactorsSetupForUser` function return `totp` as part of the array. Likewise, if the user has completed `otp-email` or `link-email` once, only then will these factors be a part of the returned array. Let's take two examples: +- The first time the user enables TOTP, then the result of `getFactorsSetupForUser` will not contain `"totp"`. So you should redirect the user to the totp setup screen. Once they add and verify a device, then `getFactorsSetupForUser` will return `["totp"]` even if they later disable totp from the settings page and renable it. +- Let's say that the first factor for a user is `emailpassword`, and the second factor is `otp-email`. Once they sign up, SuperTokens already knows the email for the user, so when they are doing the `otp-email` step, then they will not be asked to enter their email again (i.e. an OTP will be sent to them directly). However, until they actually complete the OTP flow, `getFactorsSetupForUser` will not return `["otp-email"]` as part of the output. + +:::caution +In the edge case that the a factor is enabled for a user, but they sign out before setting it up, then when they login next, SuperTokens will still ask them to complete the factor at that time. If SuperTokens doesn't have the required information (like no TOTP device for totp auth), then users will be asked to setup a device at that point in time. + +If you would like to change how this works and only want users to setup their factor via the settings page, and not during sign in, you can do so by overriding the `getMFARequirementsForAuth` function, which takes as an input the list of factors that are setup for the current user. +::: + +The subsequent sections in this doc will walk through frontend setup, and also specific examples of common MFA flows. + +## Effect on post sign up / sign in overrides +It's a very common use case to want to override the default behaviour of SuperTokens after a user signs up or signs in. For example, you may want to changes your database state whenever someone signs up. This is done by overriding the sign up / sign in recipe functions in the backend SDK: +- [ThirdPartyEmailPassword recipe](/docs/thirdpartyemailpassword/common-customizations/handling-signinup-success) +- [Passwordless recipe](/docs/passwordless/common-customizations/handling-signinup-success) +- [EmailPassword recipe](/docs/emailpassword/common-customizations/handling-signup-success) +- [ThirdPartyPasswordless recipe](/docs/thirdpartypasswordless/common-customizations/handling-signinup-success) +- [ThirdParty recipe](/docs/thirdparty/introduction) + +Now since the sign up / sign in APIs are shared for first factor and second factor login, your override will be called for both first and second factor login. So if you want to have different behaviour for first and second factor login, you can use the `input` argument to the function to determine if the user is doing first or second factor login. + +The `input` argument contains the `session` object using which you can determine if the user is doing first or second factor login. If the `session` property is `undefined`, it means it's a first factor login, else it's a second factor login. In the links above, the code snippets we have check for `input.session === undefined` to determine if it's a first factor login. + +## Enabling account linking only for MFA purposes + +The step 1 above, enables account linking for first factor login and also for MFA purposes. However, if you want to enable account linking only for MFA purposes, you can do this in the following way: + + + + +```ts +import SuperTokens, { User, RecipeUserId, } from "supertokens-node"; +import { UserContext } from "supertokens-node/types"; +import AccountLinking from "supertokens-node/recipe/accountlinking" +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +SuperTokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + // ... + // highlight-start + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { + if (session === undefined) { + // we do not want to do first factor account linking by default. To enable that, + // please see the automatic account linking docs in the recipe docs for your first factor. + return { + shouldAutomaticallyLink: false + }; + } + if (user === undefined || session.getUserId() === user.id) { + // if it comes here, it means that a session exists, and we are trying to link the + // newAccountInfo to the session user, which means it's an MFA flow, so we enable + // linking here. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + return { + shouldAutomaticallyLink: false + }; + } + }), + // highlight-end + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + \ No newline at end of file diff --git a/v2/mfa/backup-codes.mdx b/v2/mfa/backup-codes.mdx new file mode 100644 index 000000000..4d922e069 --- /dev/null +++ b/v2/mfa/backup-codes.mdx @@ -0,0 +1,122 @@ +--- +id: backup-codes +title: Implementing backup / recovery codes +hide_title: true +show_ui_switcher: true +--- + +import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" + +# Implementing backup / recovery codes + +Backup codes is one of the ways in which end users can recover their account in case they loose their second factor device. At the moment, SuperTokens does not have an in built implementation for backup codes, however, you can customise our SDKs to add this feature. This guide will show you how to implement backup codes in your application. + +:::note +[Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes) of how you can implement backup codes in your application if you are using our pre built UI. +::: + +## How it works +We allow users to generate a backup code after they have setup MFA. This backup code will be associated with their userID in their user metadata JSON. Once users want to use their backup code, we show them a UI to enter their code which calls an API that verifies the backup code and adds a flag in their session indicating that they have correctly supplied a backup code. Post that, we use this flag to allow users to create a new MFA device (for example a new TOTP device), even if they already have one registered on their account. User can then go about adding a new device and completing MFA using that. + +:::important +The guide below focuses on TOTP as a second factor, but something similar can be implemented for Passwordless as well. +::: + +## Step 1: Adding an API to generate backup codes on the backend + +Here is an [example API](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/index.ts#L52) for how you can create a recovery code. In here, we create a secure random string, and save the hashed version in the user metadata JSON. This API returns the plain text recovery code to the frontend to display to the user. + +## Step 2: Allowing users to generate a backup code when they finish MFA setup + + + + + +After the user has successfully setup their second factor (during sign up or during recovery process), we navigate them to a page which shows them their backup code. + +We can redirect the user to this page automatically (once they have completed thier MFA setup) by adding a claim validator on the frontend which enforces that the user always has a backup code associated with their account, so even if they consume the code in the future, this validator will fail and redirect them to the create new backup code screen. The idea here is that we modify the access token payload on the backend to add a boolean value to it which is `true` if the user already has a backup code with their account, else it's `false`. The frontend claim validator will fail if the value is `false` and redirect to them to the UI for where we want them to create a backup code. You can see this validator implementation on the frontend [here](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/recoveryCodeExistsClaim.ts). This validator is added to the [Session.init on the frontend](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/config.tsx#L56) so that it runs each time you protect a route with ``. + +We of course also need to add a claim validator on the backend to add the boolean value to the access token paylaod. You can see the claim validator's implementation [here](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/recoveryCodeExistsClaim.ts). We use this validator in a few places: +- [When we create a new backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/index.ts#L61). +- [When we consume an existing backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L55). +- [When we create a new session](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L127). + +Once on the page, the UI calls the API we created in the previous step to create a new recovery code for the user. Note that calling this API will replace the older recovery code, but of course, this it's all custom, you can change the logic. + +[Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/CreateRecoveryCode/index.tsx) of how to implement this page. + + + + + +For custom UI, the UI for where and how you show the recovery code page is up to you. We recommend showing the user this page post sign up, or whenever they create a new MFA device successfully. + +We can redirect the user to this page automatically (once they have completed thier MFA setup) by adding a claim validator on the frontend which enforces that the user always has a backup code associated with their account, so even if they consume the code in the future, this validator will fail and redirect them to the create new backup code screen. The idea here is that we modify the access token payload on the backend to add a boolean value to it which is `true` if the user already has a backup code with their account, else it's `false`. The frontend claim validator will fail if the value is `false` and redirect to them to the UI for where we want them to create a backup code. You can see this validator implementation on the frontend [here](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/recoveryCodeExistsClaim.ts). This validator is added to the [Session.init on the frontend](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/config.tsx#L56) so that it runs each time you protect a route with `await Session.validateClaims` function call. + +We of course also need to add a claim validator on the backend to add the boolean value to the access token paylaod. You can see the claim validator's implementation [here](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/recoveryCodeExistsClaim.ts). We use this validator in a few places: +- [When we create a new backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/index.ts#L61). +- [When we consume an existing backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L55). +- [When we create a new session](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L127). + +Once on the page, the UI calls the API we created in the previous step to create a new recovery code for the user. Note that calling this API will replace the older recovery code, but of course, this it's all custom, you can change the logic. + +[Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/CreateRecoveryCode/index.tsx) of how to implement this page. + + + + + +## Step 3: Showing users UI to use their backup code on the MFA challenge UI + + + + + +This can be achieved by creating a "Lost device?" button in the pre built UI that asks the user to enter the TOTP challenge. Once they click on this, users will be redirected to a page in which they can enter their backup code, after which (if verified), they will be further redirected to the create a new TOTP device page. + +[Here is how you can override our pre built UI](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/App.tsx#L19) to display the "Lost device?" button. + +[Here is an example implementation](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/RecoveryCode/index.tsx) of a page which asks the user to enter their backup code and calls as API (see next step) to check if the code is correct or not. + + + + + +You should make a UI that asks the user to enter their backup code and call the API to verify it and mark it as "in use" (see next step). You want to give the option for users to enter their backup code when asked for the MFA challenge. + + + + + +## Step 4: Modifying the user's session to mark that they have verified their backup code + +On the backend, we [create an API](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/index.ts#L64) which accepts the recovery code entered by the user, checks that it matches the hashed version stored in the user's metadata JSON, and if it does, we mark it as "in use" in the user's session payload. We do this by saving the `recoverCodeHash` in the session payload which will then later be checked in the next step to force enable TOTP device creation. + + + + + +On the frontend, once this API returns a success, we should navigate the user to the create a new TOTP device screen. You can see how this is done [here](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/RecoveryCode/index.tsx#L20). + + + + + +On the frontend, once this API returns a success, we should navigate the user to the create a new TOTP device screen. + + + + + +## Step 5: Force allowing users to setup a new device + +There are the steps here: +- Force enabling TOTP device creation if the `recoveryCodeHash` is in the user's session's access token payload. +- Deleting the recovery code from the user's metadata JSON once they have consumed it to create a new device. +- Redirecting the user to the page that shows their new recovery code after they have setup their new device. + +The first step can be achieved by overriding [this function](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L68) on the backend. By default, the `assertAllowedToSetupFactorElseThrowInvalidClaimError` function will throw an error in case a TOTP device already exists and the user is trying to setup a new TOTP device during the sign in process (for security reasons). However, we modify this to check if the access token payload contains the `recoveryCodeHash` and that it matches the one in the user metadata JSON. If it does, we allow new device setup, since we know that the user had previously entered their recovery code successfully. + +During the TOTP device setup, users [this API](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L45) will be called from the frontend. If this API succeeds, then it means that a new TOTP device was created for the user and they have completed the TOTP challenge for the current session. So on success, we remove the old recovery code from the metadata in case the session has the `recoveryCodeHash` set. This way, the old recovery code is no longer usable. + +Finally, after a successful creation of a new TOTP device, the frontend should redirect the user to the page which shows the new recovery code for the user (see step 2 above). \ No newline at end of file diff --git a/v2/mfa/email-sms-otp/embed.mdx b/v2/mfa/email-sms-otp/embed.mdx new file mode 100644 index 000000000..c1d257169 --- /dev/null +++ b/v2/mfa/email-sms-otp/embed.mdx @@ -0,0 +1,388 @@ +--- +id: embed +title: Embed our pre built UI component +hide_title: true +show_ui_switcher: true +--- + +import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" +import FrontendPreBuiltUITabs from "/src/components/tabs/FrontendPreBuiltUITabs" +import TabItem from '@theme/TabItem'; +import {Question, Answer}from "/src/components/question" +import RRDVersionSubTabs from "/src/components/tabs/RRDVersionSubTabs" + +# Embed our pre built UI component + + + + + +:::important +The snippets below assume that you are using the Passwordless recipe for Email / SMS OTP. But if you are using ThirdPartyPasswordless, that works as well. +::: + +## Case 1: Rendering the Widget in a page + +The following example shows the scenario where you have a dedicated route, such as `/otp`, for rendering the Email / SMS OTP Widget. Upon a successful login, the user will be automatically redirected to the return value of `getRedirectionURL` (defaulting to `/`). + + + + + + + + + + +```tsx +import SuperTokens from "supertokens-auth-react"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +// highlight-start +import { MfaOtpEmail } from 'supertokens-auth-react/recipe/passwordless/prebuiltui'; +// highlight-end +// @ts-ignore +import Header from "./header"; +// @ts-ignore +import Footer from "./footer"; +import { useNavigate } from "react-router-dom"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL", + // highlight-start + mfaFeature: { + disableDefaultUI: true + } + // highlight-end + }), + MultiFactorAuth.init({ + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "GO_TO_FACTOR") { + if (context.factorId === "otp-email") { + return "/otp" + } + } + } + // highlight-end + }) + // ... + ], +}); + +function OTPPage() { + const navigate = useNavigate(); + return ( +
      +
      + // highlight-next-line + +
      +
      + ); +} +``` + +
      + + + +```tsx +import SuperTokens from "supertokens-auth-react"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +// highlight-start +import { MfaOtpEmail } from 'supertokens-auth-react/recipe/passwordless/prebuiltui'; +// highlight-end +// @ts-ignore +import Header from "./header"; +// @ts-ignore +import Footer from "./footer"; +import { useHistory } from "react-router-dom5"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL", + // highlight-start + mfaFeature: { + disableDefaultUI: true + } + // highlight-end + }), + MultiFactorAuth.init({ + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "GO_TO_FACTOR") { + if (context.factorId === "otp-email") { + return "/otp" + } + } + } + // highlight-end + }) + // ... + ], +}); + +function OTPPage() { + const history = useHistory(); + return ( +
      +
      + // highlight-next-line + +
      +
      + ); +} +``` + +
      + +
      + +
      + + +```tsx +import SuperTokens from "supertokens-auth-react"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +// highlight-start +import { MfaOtpEmail } from 'supertokens-auth-react/recipe/passwordless/prebuiltui'; +// highlight-end +// @ts-ignore +import Header from "./header"; +// @ts-ignore +import Footer from "./footer"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL", + // highlight-start + mfaFeature: { + disableDefaultUI: true + } + // highlight-end + }), + MultiFactorAuth.init({ + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "GO_TO_FACTOR") { + if (context.factorId === "otp-email") { + return "/otp" + } + } + } + // highlight-end + }) + // ... + ], +}); + +function OTPPage() { + return ( +
      +
      + // highlight-next-line + +
      +
      + ); +} +``` +
      +
      +
      +
      + +In the above code snippet, we: + +1. Disabled the default Passwordless MFA UI by setting `disableDefaultUI` to `true` inside the Passwordless recipe config. +2. Overrode the `getRedirectionURL` function inside the MFA recipe config to redirect to `/otp` whenever we want to show the OTP factor. +3. The above snippet uses `MfaOtpEmail` for otp-email factor, but if you are using `otp-phone`, then you can use the `MfaOtpPhone` component. Be sure to also change the `contactMethod` prop in Passwordless.init to use `PHONE` or `EMAIL_OR_PHONE`, depending on if you are using emial otp / link as the first factor or not. + +Feel free to customize the redirection URLs as needed. + +## Case 2: Rendering the Widget in a popup + +The following example shows the scenario where you embed the Email / SMS OTP Widget in a popup, and upon successful login, you aim to close the popup. This is especially useful for step up auth. + + + + + + + + + + +```tsx +import React, { useState, useEffect } from "react"; +import Modal from "react-modal"; +import SuperTokens from "supertokens-auth-react"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +// highlight-start +import { MfaOtpEmail } from 'supertokens-auth-react/recipe/passwordless/prebuiltui'; +import Session from "supertokens-auth-react/recipe/session"; +// highlight-end +import { useNavigate } from "react-router-dom"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL", + }), + MultiFactorAuth.init(/* ... */) + // ... + ], +}); + +function OTPPopup() { + let sessionContext = Session.useSessionContext(); + const navigate = useNavigate(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const openModal = () => setIsModalOpen(true); + const closeModal = () => setIsModalOpen(false); + + if (sessionContext.loading) { + return null; + } + + return ( +
      + { + +

      You are logged In!

      +

      UserId: {sessionContext.userId}

      + +
      + } + + + {/* highlight-next-line */} + + +
      + ); +} +``` + +
      + + + +```tsx +import React, { useState, useEffect } from "react"; +import Modal from "react-modal"; +import SuperTokens from "supertokens-auth-react"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +// highlight-start +import { MfaOtpEmail } from 'supertokens-auth-react/recipe/passwordless/prebuiltui'; +import Session from "supertokens-auth-react/recipe/session"; +// highlight-end + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL", + }), + MultiFactorAuth.init(/* ... */) + // ... + ], +}); + +function OTPPopup() { + let sessionContext = Session.useSessionContext(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const openModal = () => setIsModalOpen(true); + const closeModal = () => setIsModalOpen(false); + + if (sessionContext.loading) { + return null; + } + + return ( +
      + { + +

      You are logged In!

      +

      UserId: {sessionContext.userId}

      + +
      + } + + + {/* highlight-next-line */} + + +
      + ); +} +``` + +
      + +
      + +
      + + +```tsx +history +``` + +
      +
      +
      + +The above snippet uses `MfaOtpEmail` for otp-email factor, but if you are using `otp-phone`, then you can use the `MfaOtpPhone` component. Be sure to also change the `contactMethod` prop in Passwordless.init to use `PHONE` or `EMAIL_OR_PHONE`, depending on if you are using emial otp / link as the first factor or not. + +
      + + +This guide is not applicable for Custom UI. + + +
      \ No newline at end of file diff --git a/v2/mfa/email-sms-otp/otp-for-all-users.mdx b/v2/mfa/email-sms-otp/otp-for-all-users.mdx new file mode 100644 index 000000000..cb4fdeb21 --- /dev/null +++ b/v2/mfa/email-sms-otp/otp-for-all-users.mdx @@ -0,0 +1,741 @@ +--- +id: otp-for-all-users +title: OTP required for all users +hide_title: true +show_ui_switcher: true +--- + +import MFAPaidBanner from '../../community/reusableMD/mfa/MFAPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; +import TabItem from '@theme/TabItem'; +import PythonSyncAsyncSubTabs from "/src/components/tabs/PythonSyncAsyncSubTabs"; +import CoreInjector from "/src/components/coreInjector" +import CustomAdmonition from "/src/components/customAdmonition" +import {Question, Answer}from "/src/components/question" +import FrontendPreBuiltUITabs from "/src/components/tabs/FrontendPreBuiltUITabs" +import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" +import FrontendCustomUITabs from "/src/components/tabs/FrontendCustomUITabs" +import NpmOrScriptTabs from "/src/components/tabs/NpmOrScriptTabs" +import AppInfoForm from "/src/components/appInfoForm" +import FrontendMobileSubTabs from "/src/components/tabs/FrontendMobileSubTabs" + + + +# OTP required for all users + +In this page, we will show you how to implement an MFA policy that requires all users to complete a OTP challenge before they get access to your application. The OTP can be sent via emial or phone. + +:::note +We assume that the first factor is [email password or social login](/docs/thirdpartyemailpassword/introduction), but the same set of steps will be applicable for other first factor types as well. +::: + +## Single tenant setup + +### Backend setup +To start with, we configure the backend in the following way: + + + + +```ts +import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import { UserContext } from "supertokens-node/types"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import Passwordless from "supertokens-node/recipe/passwordless" +import Session from "supertokens-node/recipe/session" +import AccountLinking from "supertokens-node/recipe/accountlinking" +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + ThirdPartyEmailPassword.init({ + //... + }), + // highlight-start + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE" + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { + // This will enable first factor account linking. + // For example, if a user logs in via email password with email e1, + // and then signs out and logs in via Google with the same email, + // we will link the accounts (as long as the email password user's email is + // verified). + // It will also enable account linking for MFA purposes. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + }), + // highlight-end + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + return [MultiFactorAuth.FactorIds.OTP_EMAIL] + } + } + } + } + // highlight-end + }) + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +- Notice that we have initialised the Passwordless recipe in the `recipeList`. In this example, we only want to enable email based OTP, so we set the `contactMethod` to `EMAIL` and `flowType` to `USER_INPUT_CODE` (i.e. otp). If instead, you want to use phone sms based OTP, you should set the contact method to `PHONE`. If you want to give users both the options, or for some users use email, and for others use phone, you should set `contactMethod` to `EMAIL_OR_PHONE`. +- We have also enabled the account linking feature since [it's required for MFA to work](../important-concepts#relation-of-account-linking-and-mfa). The above enables account linking for first and second factor login, but if you only want to enable it for second factor, see [this section](../backend-setup#enabling-account-linking-only-for-mfa-purposes). +- We also override the `getMFARequirementsForAuth` function to indicate that `otp-email` must be completed before the user can access the app. Notice that we do not check for the userId there, and return `otp-email` for all users. You can also return `otp-phone` instead if you want users to complete the OTP challenge via a phone SMS. Finally, if you want to give users an option for email or phone, you can return the following array from the function: + + ```json + [{ + "oneOf": ["otp-email", "otp-phone"] + }] + ``` + + +Once the user finishes the first factor (for example, with emailpassword), their session access token payload will look like this: +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + }, + "v": false + } +} +``` + +The `v` being `false` indicates that there are still factors that are pending. After the user has finished `otp-email`, the payload will look like: + +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "otp-email": 1702877999 + }, + "v": true + } +} +``` + +Indicating that the user has finished all required factors, and should be allowed to access the app. + +:::caution +If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialise the Passwordless recipe again. You just have to make sure that the `contactMethod` and `flowType` are set correctly. +::: + + +### Frontend setup + + + + + +We start by modifying the `init` function call on the frontend like so: + + + + + +```tsx +import supertokens from "supertokens-auth-react" +import ThirdPartyEmailPassword from "supertokens-auth-react/recipe/thirdpartyemailpassword" +import Passwordless from "supertokens-auth-react/recipe/passwordless" +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" + +supertokens.init({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdPartyEmailPassword.init( /* ... */), + // highlight-start + Passwordless.init({ + contactMethod: "EMAIL" + }), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"] + }) + // highlight-end + ] +}) +``` + + + + + +- Just like on the backend, we init the `passwordless` recipe in the `recipeList`. The `contactMethod` needs to be consistent with the backend setting. +- We also init the `MultiFactorAuth` recipe, and pass in the first factors that we want to use. In this case, that would be `emailpassword` and `thirdparty` - same as the backend. + +Next, we need to add the Passwordless pre built UI when rendering the SuperTokens component: + + + + + + + + +```tsx +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import { ThirdPartyEmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui"; +import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; +import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; + +function App() { + return ( + +
      + +
      + + // highlight-start + {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ThirdPartyEmailPasswordPreBuiltUI, PasswordlessPreBuiltUI])} + // highlight-end + // ... other routes + +
      +
      +
      +
      + ); +} +``` + +
      + + + +```tsx +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; +import { ThirdPartyEmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui"; +import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; + +function App() { + // highlight-start + if (canHandleRoute([ThirdPartyEmailPasswordPreBuiltUI, PasswordlessPreBuiltUI])) { + return getRoutingComponent([ThirdPartyEmailPasswordPreBuiltUI, PasswordlessPreBuiltUI]) + } + // highlight-end + return ( + {/*Your app*/} + ); +} +``` + + + +
      + +
      + +
      + +With the above configuration, users will see emailpassword or social login UI when they visit the auth page. After completing that, users will be redirected to `/auth/mfa/otp-email` (assuming that the `websiteBasePath` is `/auth`) where they will be asked to complete the OTP challenge. The UI for this screen looks like: +- [Factor Setup UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/passwordless-mfa--setup-single-next-option) (This is in case the first factor doesn't provide an email for the user. In our example, the first factor does provide an email since it's email password or social login). +- [Verification UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/passwordless-mfa--verification-with-single-next-option). + +:::caution +If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialise the Passwordless recipe again. You just have to make sure that the `contactMethod` is set correctly. +::: + +
      + + + +We start by initialising the MFA and Passwordless recipe on the frontend like so: + + + + + + + + + +```tsx +import SuperTokens from 'supertokens-web-js'; +import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; +import Passwordless from "supertokens-web-js/recipe/passwordless"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + // highlight-start + MultiFactorAuth.init(), + Passwordless.init() + // highlight-end + ], +}); +``` + + + + + +```tsx +import supertokens from "supertokens-web-js-script"; +import supertokensMultiFactorAuth from 'supertokens-web-js-script/recipe/multifactorauth'; +import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; +supertokens.init({ + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + // highlight-start + supertokensMultiFactorAuth.init(), + supertokensPasswordless.init() + // highlight-end + ], +}); +``` + + + + + + + + + + +:::success +This step is not applicable for mobile apps. Please continue reading. +::: + + + + + +After the first factor login, you should start by [checking the access token payload and see if the MFA claim's `v` boolean is `false`](../frontend-setup#step-2-checking-the---custv-boolean-value-in-the-mfa-claim--cust). If it's not, then we can redeirect the user to the application page. + +If it's `false`, the frontend then needs to [call the MFA endpoint](../frontend-setup#mfa-info-endpoint) to get information about which factor the user should be asked to complete next. Based on the backend config in this page, the `next` array will contain `["otp-email"]`. + +There are two possibilities here: +- Case 1: The user needs to setup an email to send the OTP to. This only happens the first factor doesn't provide an email from the user (for example, if you used phone based otp as the first factor). In our example on this doc, we always get an email from the first factor, so you do not need to build UI for this step (but we will still talk about this later on). +- Case 2: The user already has an email associated with them and need to complete the OTP challenge. + +We can know which case it is by checking if the `emails` object returned from [MFA Info endpoint](../frontend-setup#mfa-info-endpoint) contains any emails associated with the `otp-email` key. If the `emails["otp-email"]` property of the response is `undefined` or an empty array, then it's case 1, else it's case 2. + + +#### Case 1 implementation: User needs to enter their email +In this case, we need to create a form wherein the user can enter their email. Once they submit the form, we need to call the [createCode API](/docs/passwordless/custom-ui/login-otp). + +After this API call, you can show the user the enter OTP screen, and call the [consumeCode API](/docs/passwordless/custom-ui/login-otp#step-3-verifying-the-input-otp). + +#### Case 2 implementation: User needs to complete the OTP challenge + +This case is when the user already has an email associated with their account and you can directly send a code to that email. You can get the email to send the code to from the result of the [MFA Info endpoint](../frontend-setup#mfa-info-endpoint). Specifically, from the response, you can read the email from the `emails` property like so: `emails["otp-email"][0]`. We are picking the first item in the array of email since the emails are ordered based on: +- Index 0 will contain the email that belongs to the session's user. So if the user's first factor was email password, the email in the 0th index of the array is that email. +- The other emails in the array (if they exist), are from other login methods for this user ordered based on the oldest login method first. + +You can even show a UI here asking the user to pick an email from the array if you like. Either way, when you have an email, you can all the [createCode API](/docs/passwordless/custom-ui/login-otp) to send the code to that email. + +After this API call, you can show the user the enter OTP screen, and call the [consumeCode API](/docs/passwordless/custom-ui/login-otp#step-3-verifying-the-input-otp). + +:::note +Notice that in Case 2, there is no UI for the user to enter an email. That happens silently. The user only sees the enter OTP screen. +::: + +On successful verification of the code, the `otp-email` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](../frontend-setup#mfa-info-endpoint). + + + +
      + +## Multi tenant setup + +In a multi tenancy setup, you may want to enable email / phone OTP for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#single-tenant-setup) section above, so in this section, we will focus on enabling OTP for all users within specific tenants. + +### Backend setup + +To start, we will initialise the Passwordless and the MultiFactorAuth recipes in the following way: + + + + +```ts +import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import { UserContext } from "supertokens-node/types"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import Passwordless from "supertokens-node/recipe/passwordless" +import Session from "supertokens-node/recipe/session" +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + ThirdPartyEmailPassword.init({ + //... + }), + // highlight-start + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE" + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { + // This will enable first factor account linking. + // For example, if a user logs in via email password with email e1, + // and then signs out and logs in via Google with the same email, + // we will link the accounts (as long as the email password user's email is + // verified). + // It will also enable account linking for MFA purposes. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + }), + MultiFactorAuth.init() + // highlight-end + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +Unlike the single tenant setup, we do not provide any config to the `MultiFactorAuth` recipe cause all the necessary configuration will be done on a tenant level. + +To configure otp-email requirement for a tenant, we can call the following API: + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function createNewTenant() { + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + emailPasswordEnabled: true, + thirdPartyEnabled: true, + passwordlessEnabled: true, + firstFactors: ["emailpassword", "thirdparty"], + requiredSecondaryFactors: ["otp-email"] + }); + + if (resp.createdNew) { + // Tenant created successfully + } else { + // Existing tenant's config was modified. + } +} +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + + + + +```bash +curl --location --request PUT '^{coreInjector_uri_without_quotes}/appid-/recipe/multitenancy/tenant' \ +--header 'api-key: ^{coreInjector_api_key_without_quotes}' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "tenantId": "customer1", + "emailPasswordEnabled": true, + "thirdPartyEnabled": true, + "passwordlessEnabled": true, + "firstFactors": ["emailpassword", "thirdparty"], + "requiredSecondaryFactors": ["otp-email"] +}' +``` + + + + + + + +- In the above, we set the `firstFactors` to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either emailpassword or thirdparty. We also configure that `emailPasswordEnabled` and `thirdPartyEnabled` are enabled for the tenant. +- We set the `requiredSecondaryFactors` to `["otp-email"]` to indicate that OTP email is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. We also set `passwordlessEnabled` to `true` since the `otp-email` factor is provided by the `Passwordless` recipe. + +Once the user finishes the first factor (for example, with emailpassword), their session access token payload will look like this: +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + }, + "v": false + } +} +``` + +The `v` being `false` indicates that there are still factors that are pending. After the user has finished otp-email challenge, the payload will look like: + +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "otp-email": 1702877999 + }, + "v": true + } +} +``` + +Indicating that the user has finished all required factors, and should be allowed to access the app. + +:::caution +If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialise the Passwordless recipe again. You just have to make sure that the `contactMethod` and `flowType` are set correctly. +::: + + +### Frontend setup + + + + + +We start by modifying the `init` function call on the frontend like so: + + + + + +```tsx +import supertokens from "supertokens-auth-react" +import ThirdPartyEmailPassword from "supertokens-auth-react/recipe/thirdpartyemailpassword" +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" +import Passwordless from "supertokens-auth-react/recipe/passwordless" +import Multitenancy from "supertokens-auth-react/recipe/multitenancy" + +supertokens.init({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + // highlight-next-line + usesDynamicLoginMethods: true, + recipeList: [ + ThirdPartyEmailPassword.init( /* ... */), + // highlight-start + Passwordless.init({ + contactMethod: "EMAIL" + }), + MultiFactorAuth.init(), + Multitenancy.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getTenantId: async (context) => { + return "TODO" + } + } + } + } + }) + // highlight-end + ] +}) +``` + + + + + +- Just like on the backend, we init the `Passwordless` recipe in the `recipeList`. Make sure that the config for it is consistent with whats on the backend. +- We also init the `MultiFactorAuth` recipe. Notice that unlike the single tenant setup, we do not specify the `firstFactors` here. That information is fetched based on the tenantId you provide the SDK with. +- We have set `usesDynamicLoginMethods: true` so that the SDK knows to fetch the login methods dynamically based on the tenantId. +- Finally, we init the multi tenancy recipe and provide a method for getting the tenantId. + +Next, we need to add the Passwordless pre built UI when rendering the SuperTokens component: + + + + + + + + +```tsx +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import { ThirdPartyEmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui"; +import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; +import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; + +function App() { + return ( + +
      + +
      + + // highlight-start + {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ThirdPartyEmailPasswordPreBuiltUI, PasswordlessPreBuiltUI])} + // highlight-end + // ... other routes + +
      +
      +
      +
      + ); +} +``` + +
      + + + +```tsx +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; +import { ThirdPartyEmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui"; +import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; + +function App() { + // highlight-start + if (canHandleRoute([ThirdPartyEmailPasswordPreBuiltUI, PasswordlessPreBuiltUI])) { + return getRoutingComponent([ThirdPartyEmailPasswordPreBuiltUI, PasswordlessPreBuiltUI]) + } + // highlight-end + return ( + {/*Your app*/} + ); +} +``` + + + +
      + +
      + +
      + +With the above configuration, users will see the first and second factor based on the tenant configuration. For the tenant we configured above, users will see email password or social login first. After completing that, users will be redirected to `/auth/mfa/otp-email` (assuming that the `websiteBasePath` is `/auth`) where they will be asked complete the OTP challenge. The UI for this screen looks like: +- [Factor Setup UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/passwordless-mfa--setup-single-next-option) (This is in case the first factor doesn't provide an email for the user. In our example, the first factor does provide an email since it's email password or social login). +- [Verification UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/passwordless-mfa--verification-with-single-next-option). + +:::caution +If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialise the Passwordless recipe again. You just have to make sure that the `contactMethod` is set correctly. +::: + +
      + + + +The steps here are the same as in [the single tenant setup above](#frontend-setup). + + + +
      + + + + + +## Protecting frontend and backend routes + +See the section on [protecting frontend and backend routes](../protect-routes). + +## Email / SMS sending and design +By default, the email template used for otp-email login is [as shown here](https://github.com/supertokens/email-sms-templates?tab=readme-ov-file#otp-login), and the default SMS template is [as shown here](https://github.com/supertokens/email-sms-templates?tab=readme-ov-file#otp-login-1). The method for sending them is via an email and sms sending service that we provide. + +If you would like to learn more about this, or change the content of the email, or the method by which they are sent, checkout the email / sms delivery section in the recipe docs: +- [Passwordless email delivery config](/docs/passwordless/email-delivery/about) +- [ThirdPartyPasswordless email delivery config](/docs/thirdpartypasswordless/email-delivery/about) +- [Passwordless SMS delivery config](/docs/passwordless/sms-delivery/about) +- [ThirdPartyPasswordless SMS delivery config](/docs/thirdpartypasswordless/sms-delivery/about) + + \ No newline at end of file diff --git a/v2/mfa/email-sms-otp/otp-for-opt-in-users.mdx b/v2/mfa/email-sms-otp/otp-for-opt-in-users.mdx new file mode 100644 index 000000000..1d1a91c2e --- /dev/null +++ b/v2/mfa/email-sms-otp/otp-for-opt-in-users.mdx @@ -0,0 +1,611 @@ +--- +id: otp-for-opt-in-users +title: OTP for specific users +hide_title: true +--- + +import MFAPaidBanner from '../../community/reusableMD/mfa/MFAPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; +import TabItem from '@theme/TabItem'; +import PythonSyncAsyncSubTabs from "/src/components/tabs/PythonSyncAsyncSubTabs"; +import CoreInjector from "/src/components/coreInjector" +import CustomAdmonition from "/src/components/customAdmonition" +import {Question, Answer}from "/src/components/question" +import FrontendPreBuiltUITabs from "/src/components/tabs/FrontendPreBuiltUITabs" +import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" +import FrontendCustomUITabs from "/src/components/tabs/FrontendCustomUITabs" +import NpmOrScriptTabs from "/src/components/tabs/NpmOrScriptTabs" +import AppInfoForm from "/src/components/appInfoForm" +import FrontendMobileSubTabs from "/src/components/tabs/FrontendMobileSubTabs" + + + +# OTP for specific users + +In this page, we will show you how to implement an MFA policy that requires certain users to do the OTP challenge via email or sms. You can decide which those users are based on any criteria. For example: +- Only users that have an `admin` role require to do OTP; OR +- Only users that have enabled OTP on their account require to do OTP; OR +- Only users that have a paid account require to do OTP. + +Whatever the criteria is, the steps to implementing this type of a flow is the same. + +:::note +We assume that the first factor is [email password or social login](/docs/thirdpartyemailpassword/introduction), but the same set of steps will be applicable for other first factor types as well. +::: + +## Single tenant setup + +### Backend setup + +
      Example 1: Only enable OTP for users that have an `admin` role + +To start with, we configure the backend in the following way: + + + + +```ts +import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import { UserContext } from "supertokens-node/types"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import Passwordless from "supertokens-node/recipe/passwordless" +import Session from "supertokens-node/recipe/session" +import UserRoles from "supertokens-node/recipe/userroles" +import AccountLinking from "supertokens-node/recipe/accountlinking" +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + UserRoles.init(), + ThirdPartyEmailPassword.init({ + //... + }), + // highlight-start + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE" + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { + // This will enable first factor account linking. + // For example, if a user logs in via email password with email e1, + // and then signs out and logs in via Google with the same email, + // we will link the accounts (as long as the email password user's email is + // verified). + // It will also enable account linking for MFA purposes. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + }), + // highlight-end + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) + if (roles.roles.includes("admin")) { + // we only want otp-email for admins + return ["otp-email"] + } else { + // no MFA for non admin users. + return [] + } + } + } + } + } + // highlight-end + }) + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + + +We override the `getMFARequirementsForAuth` function to indicate that `otp-email` must be completed only for users that have the `admin` role. You can also have any other criteria here. + +
      + +
      Example 2: Ask for OTP only for users that have enabled OTP on their account + +To start with, we configure the backend in the following way: + + + + +```ts +import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import { UserContext } from "supertokens-node/types"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth" +import Passwordless from "supertokens-node/recipe/passwordless" +import Session from "supertokens-node/recipe/session" +import AccountLinking from "supertokens-node/recipe/accountlinking" +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + ThirdPartyEmailPassword.init({ + //... + }), + // highlight-start + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + override: { + apis: (oI) => { + return { + ...oI, + consumeCodePOST: async function (input) { + let response = await oI.consumeCodePOST!(input); + if (response.status === "OK" && input.session !== undefined) { + // We do this only if a session exists, which means that it's not being called for first factor login. + + // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata. + // The multifactorauth recipe will pick this value up next time the user is trying to login, and + // ask them to enter the OTP code. + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.OTP_EMAIL); + } + return response; + } + } + } + } + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { + // This will enable first factor account linking. + // For example, if a user logs in via email password with email e1, + // and then signs out and logs in via Google with the same email, + // we will link the accounts (as long as the email password user's email is + // verified). + // It will also enable account linking for MFA purposes. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + }), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + }) + // highlight-end + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +- We simply initialise the multi factor auth recipe here without any override to `getMFARequirementsForAuth`. The default implementation of this function already checks what factors are enabled for a user and returns those. So all we need to do is mark `otp-email` as enabled for a user as soon as they have completed the OTP challenge successfuly. This happens in the `consumeCodePOST` API override as shown above. Once the code is consumed successfully, we mark the `otp-email` factor as enabled for the user, and the next time they login, they will be asked to complete the OTP challenge. +- Notice that before we call `addToRequiredSecondaryFactorsForUser`, we check if there is an input session or not. We only want to call `addToRequiredSecondaryFactorsForUser` function if there is a session which indicates that the user has finished some first factor already. + +
      + +In both of the examples above, notice that we have initialised the Passwordless recipe in the `recipeList`. In this example, we only want to enable email based OTP, so we set the `contactMethod` to `EMAIL` and `flowType` to `USER_INPUT_CODE` (i.e. otp). If instead, you want to use phone sms based OTP, you should set the contact method to `PHONE`. If you want to give users both the options, or for some users use email, and for others use phone, you should set `contactMethod` to `EMAIL_OR_PHONE`. + +We have also enabled the account linking feature since [it's required for MFA to work](../important-concepts#relation-of-account-linking-and-mfa). The above enables account linking for first and second factor login, but if you only want to enable it for second factor, see [this section](../backend-setup#enabling-account-linking-only-for-mfa-purposes). + +Once the user finishes the first factor (for example, with emailpassword), their session access token payload will look like this (for those that require OTP): +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + }, + "v": false + } +} +``` + +The `v` being `false` indicates that there are still factors that are pending. After the user has finished otp-email, the payload will look like: + +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "otp-email": 1702877999 + }, + "v": true + } +} +``` + +Indicating that the user has finished all required factors, and should be allowed to access the app. + +:::caution +If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialise the Passwordless recipe again. +::: + +### Frontend setup + +There are two parts to this: +- Configuring the frontend to show the OTP challenge UI when required during login / sign up +- Allowing users to enable / disable OTP challenge on their account via the settings page (If you are following Example 2 from above). + +The first part is identical to the steps mentioned in [this section](./otp-for-all-users#frontend-setup), so please follow that. + +The second part, which is only applicable in case you want to allow users to enable / disable OTP themselves, can be achieved by creating the following flow on your frontend: +- When the user navigates to their settings page, you can show them if OTP challenge is enabled or not. +- If enabled, you can allow them to disable it, or vice versa. + +In order to know if the user has enabled OTP, you can make an API your backend which calls the following function: + + + + +```ts +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +async function isOTPEmailEnabledForUser(userId: string) { + let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId) + return factors.includes("otp-email") +} +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +If the user wants to enable or disable otp-email for them, you can make an API on your backend which calls the following function: + + + + +```ts +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +async function enableMFAForUser(userId: string) { + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, "otp-email") +} + +async function disableMFAForUser(userId: string) { + await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, "otp-email") +} +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +If instead you want to work with `otp-phone`, you can replace `otp-email` with `otp-phone` in the above snippets. Also make sure that the `contactMethod` is set to `PHONE` in the Passwordless recipe on the frontend (for pre built UI) and backend. +::: + + +## Multi tenant setup + +### Backend setup +A user can be a part of several tenants. So if you want OTP to be enabled for a specific user across all the tenants that they are a part of, the steps are the same as in the [Backend setup](#backend-setup) section above. + +However, if you want OTP to be enabled for a specific user, for a specific tenant (or a sub set of tenants that the user is a part of), then you will have to add additional logic to the `getMFARequirementsForAuth` function override. Modifying the example code from the [Backend setup](#backend-setup) section above: + +
      Example 1: Only enable OTP for users that have an `admin` role + + + + +```ts +import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import { UserContext } from "supertokens-node/types"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import Passwordless from "supertokens-node/recipe/passwordless" +import Session from "supertokens-node/recipe/session" +import UserRoles from "supertokens-node/recipe/userroles" +import AccountLinking from "supertokens-node/recipe/accountlinking" +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + UserRoles.init(), + ThirdPartyEmailPassword.init({ + //... + }), + // highlight-next-line + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE" + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { + // This will enable first factor account linking. + // For example, if a user logs in via email password with email e1, + // and then signs out and logs in via Google with the same email, + // we will link the accounts (as long as the email password user's email is + // verified). + // It will also enable account linking for MFA purposes. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + }), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) + // highlight-next-line + if (roles.roles.includes("admin") && await shouldRequireOTPEmailForTenant(input.tenantId)) { + // we only want otp-email for admins + return ["otp-email"] + } else { + // no MFA for non admin users. + return [] + } + } + } + } + } + }) + ] +}) + +// highlight-start +async function shouldRequireOTPEmailForTenant(tenantId: string): Promise { + // your logic here to determine if we care about otp-email for this tenant or not. + return true; +} +// highlight-end +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +- The implementation of `shouldRequireOTPEmailForTenant` is entirely up to you. + +
      + +
      Example 2: Ask for OTP only for users that have enabled OTP on their account + + + + +```ts +import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import { UserContext } from "supertokens-node/types"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth" +import Passwordless from "supertokens-node/recipe/passwordless" +import Session from "supertokens-node/recipe/session" +import AccountLinking from "supertokens-node/recipe/accountlinking" +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + ThirdPartyEmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + override: { + apis: (oI) => { + return { + ...oI, + consumeCodePOST: async function (input) { + let response = await oI.consumeCodePOST!(input); + if (response.status === "OK" && input.session !== undefined) { + // We do this only if a session exists, which means that it's not being called for first factor login. + + // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata. + // The multifactorauth recipe will pick this value up next time the user is trying to login, and + // ask them to enter the OTP code. + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.OTP_EMAIL); + } + return response; + } + } + } + } + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { + // This will enable first factor account linking. + // For example, if a user logs in via email password with email e1, + // and then signs out and logs in via Google with the same email, + // we will link the accounts (as long as the email password user's email is + // verified). + // It will also enable account linking for MFA purposes. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + }), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + if ((await input.requiredSecondaryFactorsForUser).includes("otp-email")) { + if (await shouldRequireOTPEmailForTenant(input.tenantId)) { + return ["otp-email"] + } + } + // no otp-email required for input.user, with the input.tenant. + return [] + } + } + } + } + // highlight-end + }) + ] +}) + +// highlight-start +async function shouldRequireOTPEmailForTenant(tenantId: string): Promise { + // your logic here to determine if we care about otp-email for this tenant or not. + return true; +} +// highlight-end +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](../legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +We provide an override for `getMFARequirementsForAuth` which checks if otp-email is enabled for the user, and also take into account the tenantId to decide if we want to have this user go through the otp-email flow whilst logging into this tenant. The implementation of `shouldRequireOTPEmailForTenant` is entirely up to you. + +
      + +### Frontend setup +The frontend setup is identical to the [frontend setup](#frontend-setup) section above. + + + + + +## Protecting frontend and backend routes + +See the section on [protecting frontend and backend routes](../protect-routes). + +## Email / SMS sending and design +By default, the email template used for otp-email login is [as shown here](https://github.com/supertokens/email-sms-templates?tab=readme-ov-file#otp-login), and the default SMS template is [as shown here](https://github.com/supertokens/email-sms-templates?tab=readme-ov-file#otp-login-1). The method for sending them is via an email and sms sending service that we provide. + +If you would like to learn more about this, or change the content of the email, or the method by which they are sent, checkout the email / sms delivery section in the recipe docs: +- [Passwordless email delivery config](/docs/passwordless/email-delivery/about) +- [ThirdPartyPasswordless email delivery config](/docs/thirdpartypasswordless/email-delivery/about) +- [Passwordless SMS delivery config](/docs/passwordless/sms-delivery/about) +- [ThirdPartyPasswordless SMS delivery config](/docs/thirdpartypasswordless/sms-delivery/about) + + \ No newline at end of file diff --git a/v2/mfa/frontend-setup.mdx b/v2/mfa/frontend-setup.mdx new file mode 100644 index 000000000..b00b3d6b3 --- /dev/null +++ b/v2/mfa/frontend-setup.mdx @@ -0,0 +1,923 @@ +--- +id: frontend-setup +title: Frontend setup +hide_title: true +show_ui_switcher: true +--- + +import MFAPaidBanner from '../community/reusableMD/mfa/MFAPaidBanner.mdx' +import TabItem from '@theme/TabItem'; +import CustomAdmonition from "/src/components/customAdmonition" +import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" +import FrontendPreBuiltUITabs from "/src/components/tabs/FrontendPreBuiltUITabs" +import FrontendCustomUITabs from "/src/components/tabs/FrontendCustomUITabs" +import NpmOrScriptTabs from "/src/components/tabs/NpmOrScriptTabs" +import AppInfoForm from "/src/components/appInfoForm" +import FrontendMobileSubTabs from "/src/components/tabs/FrontendMobileSubTabs" +import {Question, Answer}from "/src/components/question" + + + +# Frontend setup + + + + + +## Single tenant setup + +The pre built UI provides support for the following MFA methods: +- TOTP +- Email / phone OTP + +If you want other types of MFA (like magic links, or password), please consider checking out the custom UI second. + +We start by initialising the MFA recipe on the frontend and providing the list of first factors as shown below: + + + + + +```tsx +import supertokens from "supertokens-auth-react" +import ThirdPartyEmailPassword from "supertokens-auth-react/recipe/thirdpartyemailpassword" +import Passwordless from "supertokens-auth-react/recipe/passwordless" +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" + +supertokens.init({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdPartyEmailPassword.init( /* ... */), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + }), + // highlight-start + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"] + }) + // highlight-end + ] +}) +``` + + + + + +In the above snippet, we have configured thirdparty and email password as first factors. The second factor is configured [on the backend](./backend-setup), and is determined based on the boolean value of [`v` in the MFA claim in the session](./important-concepts#how-are-auth-factors-marked-as-completed). If the `v` is `false` in the session, it means that there are still factors pending before the user has completed login. In this case, the frontend SDK calls the MFAInfo endpoint (see more about this later) on the backend which returns the list of factors (`string[]`) that the user must complete next. For example: +- If the next array is `["otp-email"]`, then we will show the user the enter OTP screen for the email associated with the first factor login. +- If the `n` array has multiple items, we will show the user a [factor chooser screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with. +- If the `next` is empty, it means that: + - There is a misconfig on the backend. This would show an access denied screen to the user. OR; + - There is another claim that needs to be finished first (like email verification), before the next MFA challenge can be shown. This can happen if you configure the backend's `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email is verified. + +If you notice, in the above code snippet, we have added `Passwordless.init` as well, and this handles cases where the second factor is `otp-email` or `otp-sms`. For TOTP, we have a different recipe as shown later in this guide. + +## Multi tenant setup +For a multi factor setup, the first factors is decided based on [the configuration of the tenant](./backend-setup#multi-tenant-setup). Each tenant has a `firstFactors` array configuration which will determine the login options shown for that tenant. For MFA, the login options will be determined by the [`requiredSecondaryFactors` config on the tenant](./backend-setup#multi-tenant-setup-1), or based on the customisations for `getMFARequirementsForAuth` on the backend. + +To tell the frontend to dynamically load the factors based on the tenant, we need to give it four things: +- The current tenantId +- Enable dynamic login methods +- Add `MultiFactorAuth.init` to the recipe list without any configured `firstFactors` +- Init all the recipes that can be possibly used by any tenant as the first or second factor. + + + + + +```tsx +import supertokens from "supertokens-auth-react" +import ThirdPartyEmailPassword from "supertokens-auth-react/recipe/thirdpartyemailpassword" +import Passwordless from "supertokens-auth-react/recipe/passwordless" +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" +import Multitenancy from "supertokens-auth-react/recipe/multitenancy" + +supertokens.init({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + // highlight-next-line + usesDynamicLoginMethods: true, + recipeList: [ + Multitenancy.init({ + override: { + functions: (oI) => { + return { + ...oI, + // highlight-start + getTenantId: (input) => { + // Implement the following based on the UX flow you want for + // tenant discovery + return "TODO.." + } + // highlight-end + } + } + } + }), + ThirdPartyEmailPassword.init( /* ... */), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + }), + // highlight-start + MultiFactorAuth.init() + // highlight-end + ] +}) +``` + + + + + +- In the above code snippet, we have added `ThirdPartyEmailPassword` and `Passwordless` as the auth methods. This works for a variety of use cases like: + - The first factor for any tenant can be third party or email password login, and the second factor can be passwordless login (`otp-email` or `otp-sms`). + - The first factor for any tenant can be email password, with, or without a second factor (like `otp-email`).. + - The first factor for any tenant can be third party, with, or without a second factor (like `otp-email`).. + - The first factor for any tenant can be passwordless login (with magic link), with or without a second factor (like `otp-email`). + - You can even change `passwordles.init` to using `thirdpartypasswordless.init` if you want to have the first factor for any tenant to be thirdparty or passwordless login, with or without a second factor (like `otp-email`). +- We have initialised `MultiFactorAuth` without any configured `firstFactors` because we want the frontend to dynamically load the first factors based on the tenant. Therefore, we have also set `usesDynamicLoginMethods: true` in the `supertokens.init` call. +- We have initialised `Multitenancy` as well, and provided a skeleton for `getTenantId`. You need to implement this function based on the UX flow you want for tenant discovery. For examlpe, [here is a common UX flow in which we decide the tenant ID based on the current sub domain](/docs/thirdpartyemailpassword/common-customizations/multi-tenancy/sub-domain-login). + +:::important +- If you do initialise the `firstFactors` array for `MultiFactorAuth.init()` on the frontend, it will be ignored when `usesDynamicLoginMethods: true` is set. +- If the tenant doesn't have the `firstFactors` array set, then the list of first factors that are rendered would be based on the [login methods that are enabled in that tenant's config](/docs/multitenancy/new-tenant#basic-tenant-creation). +::: + +The second factor for a tenant is determined based on the [`secondaryFactors` config for the tenant](./backend-setup#multi-tenant-setup-1), or based on any custom implementation for the `getMFARequirementsForAuth` function. If the current user has specific MFA methods enabled for them, those will also be shown as options as well. Overall, the list of secondary factors will be used to build the `next` array returned from the MFAInfo endpoint (see more about this later). For example: +- If the next array is `["otp-email"]`, then we will show the user the enter OTP screen for the email associated with the first factor login. +- If the `n` array has multiple items, we will show the user a [factor chooser screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with. +- If the `next` is empty, it means that: + - There is a misconfig on the backend. This would show an access denied screen to the user. OR; + - There is another claim that needs to be finished first (like email verification), before the next MFA challenge can be shown. This can happen if you configure the backend's `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email is verified. + +In the subsequent sections, we will walk through specific MFA setup examples for your reference. + +## Handling misconfigurations + +There can be situations of misconfigurations. For example you may have enabled `otp-email` for a user as a secondary factor, but did not add `Passwordless` (or `ThirdPartyPasswordless`) in the `recipeList` on the frontend. In such (and similar) situations, the pre built UI on the frontend will throw an error which will be propagated to the error boundry of your app. The way to solve these errors is to recheck the `recipeList` on the frontend, and make sure that it has all the recipes initialised that are needed for any factor configured on the backend. + +## The access denied screen + +Sometimes, users may end up seeing [an access denied screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-access-denied) during the login flow. This is shown if there is a 500 (backend sends a 500 status code) error during the MFA flow for API calls that are initiated automatically (without user action). For example: +- When the user wants to setup a new TOTP device, the pre bult UI calls the `createDevice` function from the totp recipe on page load, and if that fails, users will see the access denied screen asking them to retry. +- When the user needs to complete an OTP email factor, and if the API call to send an email (which is called on page load) fails, then users will see the access denied screen asking them to retry. + +You can override this component in the following way: + + + + + + + + +```tsx +import React from "react"; +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { SessionComponentsOverrideProvider } from "supertokens-auth-react/recipe/session"; + +function App() { + return ( + + { + return ( +
      + Access denied! + {props.error === undefined ? null : props.error} +
      + ); + }, + }}> + {/* Rest of the JSX */} +
      +
      + ); +} +export default App; +``` + +
      + + + +```tsx +import React from "react"; +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { getRoutingComponent, canHandleRoute } from "supertokens-auth-react/ui"; +import { SessionComponentsOverrideProvider } from "supertokens-auth-react/recipe/session"; + +function App() { + if(canHandleRoute([/*...*/])){ + return ( + { + return ( +
      + Access denied! + {props.error === undefined ? null : props.error} +
      + ); + }, + }}> + {getRoutingComponent([/*...*/])} +
      + ) + } + return ( + + {/* Rest of the JSX */} + + ); +} +export default App; +``` + +
      + +
      + +
      + +
      + + +
      + + + +After the first factor sign in is over, to know the next auth challenge, the frontend should rely on the session's access token payload MFA claim's `n` array. For example, the access token payload may have the following content: + +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + }, + "v": false + } +} +``` + +This means that the user has completed the email password login, and that there are still MFA login challenge(s) remaining (`v` is `false`). + +## Initialise the MFA recipe on the frontend + + + + + + + + + +```tsx +import SuperTokens from 'supertokens-web-js'; +import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + // highlight-next-line + MultiFactorAuth.init() + ], +}); +``` + + + + + +```tsx +import supertokens from "supertokens-web-js-script"; +import supertokensMultiFactorAuth from 'supertokens-web-js-script/recipe/multifactorauth'; +supertokens.init({ + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + // highlight-next-line + supertokensMultiFactorAuth.init() + ], +}); +``` + + + + + + + + + + +:::success +This step is not applicable for mobile apps. Please continue reading. +::: + + + + + +## Factor lifecycle overview + +The overall lifecycle of a factor post sign in is as follows: + +### Step 1: Asking for the first factor +This is the same as setting up a recipe as per the other recipe guides. So please follow those. + +### Step 2: Checking the `v` boolean value in the MFA claim +After the first factor has been completed, the frontend needs to check if there are any pending MFA challenges. This can be done by reading the `v` claim from the session as shown below: + + + + + + + + + +```tsx +import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; +import Session from "supertokens-web-js/recipe/session"; + +async function isAllMFACompleted() { + if (await Session.doesSessionExist()) { + let mfaClaim = await Session.getClaimValue({ + claim: MultiFactorAuth.MultiFactorAuthClaim + }); + if (mfaClaim === undefined) { + // this can happen during migration where the session is an older one + // that was created before MFA was introduced on the backend + return true; + } else { + return mfaClaim.v + } + } else { + throw new Error("Illegal function call: For first factor setup, you do not need to call this function") + } +} +``` + + + + + +```tsx +import supertokensMultiFactorAuth from 'supertokens-web-js-script/recipe/multifactorauth'; +import supertokensSession from "supertokens-web-js-script/recipe/session"; +async function isAllMFACompleted() { + if (await supertokensSession.doesSessionExist()) { + let mfaClaim = await supertokensSession.getClaimValue({ + claim: supertokensMultiFactorAuth.MultiFactorAuthClaim + }); + if (mfaClaim === undefined) { + // this can happen during migration where the session is an older one + // that was created before MFA was introduced on the backend + return true; + } else { + return mfaClaim.v + } + } else { + throw new Error("Illegal function call: For first factor setup, you do not need to call this function") + } +} +``` + + + + + + + + + + + + + + +```tsx +import SuperTokens from 'supertokens-react-native'; + +async function isAllMFACompleted() { + if (await SuperTokens.doesSessionExist()) { + + // highlight-start + let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; + return isMFACompleted + // highlight-end + } +} +``` + + + + + +```kotlin +import android.app.Application +import com.supertokens.session.SuperTokens +import org.json.JSONObject + +class MainApplication: Application() { + fun isAllMFACompleted() { + val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this); + val isMFACompleted: Boolean = (accessTokenPayload.get("st-mfa") as JSONObject).get("v") as Boolean; + return isMFACompleted; + } +} +``` + + + + + +```swift +import UIKit +import SuperTokensIOS + +fileprivate class ViewController: UIViewController { + func isAllMFACompleted() { + if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely(), let mfaObject: [String: Any] = accessTokenPayload["st-mfa"] as? [String: Any], let isMFACompleted: Boolean = mfaObject["v"] as? Boolean { + return isMFACompleted; + } + } +} +``` + + + + + +```dart +import 'package:supertokens_flutter/supertokens.dart'; + +Future isAllMFACompleted() async { + var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); + + if (accessTokenPayload.containsKey("st-mfa")) { + Map mfaObject = accessTokenPayload["st-mfa"]; + + if (mfaObject.containsKey("v")) { + Boolean isMFACompleted = mfaObject["v"]; + + return isMFACompleted; + } + } +} +``` + + + + + + + + + + +### Step 3: Checking the `next` array +Once we determine that MFA is still pending, we need to get the list of factors the user must do next. We can do this by calling the [MFA Info endpoint](#mfa-info-endpoint) which returns a list of next (`string[]`) factors: +- If there are multiple values in this array, then the frontend needs to show these options to the user and ask them to pick one of them. +- If there is only one item, then the UI can directly ask the user to complete that factor. +- If this array is empty, then: + - There is a misconfig on the backend. This would show an access denied screen to the user. OR; + - There is another claim that needs to be finished first (like email verification), before the next MFA challenge can be shown. This can happen if you configure the backend's `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email is verified. + +### Step 4: Checking for factor setup +Once the user has picked a specific factor (or if `next` contains just one item), you need to check if that factor has already been setup for that user. A factor is setup already if: +- For `totp`: The user has already added a totp device and verified it. +- For `otp-email`: The user has a passwordless loginMethod that has an email associated with it. +- For `link-email`: The user has a passwordless loginMethod that has an email associated with it. Note that this is not a valid secondary factor, but is a valid first factor. +- For `otp-sms`: The user has a passwordless loginMethod that has a phone number associated with it. +- For `link-sms`: The user has a passwordless loginMethod that has a phone number associated with it. Note that this is not a valid secondary factor, but is a valie first factor. +- For `emailpassword`: The user has an email password loginMethod. +- For `thirdparty`: The user has a third party loginMethod. + +If the user has the factor already setup, you can skip the setup step and directly ask them for the challenge: +- For `totp`: Ask them to enter the OTP. +- For `otp-email`: Send them an email with the OTP, and ask them to enter the OTP. +- For `otp-sms`: Send them an SMS with the OTP, and ask them to enter the OTP. +- For `emailpassword`: Ask them to enter their password. +- For `thirdparty`: Ask them to login using the third party provider. + +In case the user does not have the factor setup, you need to ask them to set it up first: +- For `totp`: Ask them to scan the QR code and enter the TOTP to verify the device. +- For `otp-email`: Ask them to enter their email and send them an email with the OTP. Once they enter the OTP, a passwordless user will be created and associated with their user object. Note that if you already have the user's email from another login method (see later), you do not need to ask them to enter their email again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. +- For `otp-sms`: Ask them to enter their phone number and send them an SMS with the OTP. Once they enter the OTP, a passwordless user will be created and associated with their user object. Note that if you already have the user's phone number from another login method (see later), you do not need to ask them to enter their phone number again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. +- For `emailpassword`: Ask them to enter their email and password. Once they enter the password, an email password user will be created and associated with their user object. Note that if you already have the user's email from another login method (see later), you do not need to ask them to enter their email again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. Here you would be calling the sign up API, vs in the other case (where the factor is already setup), you would be calling the sign in API. +- For `thirdparty`: Ask them to login using the third party provider. Once they login, a third party user will be created and associated with their user object. + +In the later guides of this recipe, we will walk through each of these use cases in detail. For now though, if you want to know the status of any factor, you can get that by calling the [MFA Info endpoint](#mfa-info-endpoint). + + + +
      + +## Reference section + +### MFA Info endpoint + +This is an important endpoint which can be used to: +- Know which factors are pending for the user (referred to as the the `next` array in our docs). +- Update the `v` and `c` values in the MFA claim. +- Get a list of all factors that are already setup for the session user. +- For each factor, get a list of emails / phone numbers that can be used for that factor. + +Our pre built UI uses this API automatically, but you can also always call this API manually if you are building a custom UI: + + + + + + + + + +```tsx +import MultifactorAuth from "supertokens-web-js/recipe/multifactorauth" +import Session from "supertokens-web-js/recipe/session" + +async function fetchMFAInfo() { + if (await Session.doesSessionExist()) { + try { + let mfaInfo = await MultifactorAuth.resyncSessionAndFetchMFAInfo() + let factorEmails = mfaInfo.emails; + let factorPhoneNumbers = mfaInfo.phoneNumbers; + + let emailsForOTPEmail = factorEmails["otp-email"]; + let phoneNumbersForOTPSms = factorEmails["otp-sms"]; + let isTotpSetup = mfaInfo.factors.alreadySetup.includes("totp"); + let isOTPEmailSetup = mfaInfo.factors.alreadySetup.includes("otp-email"); + let isOTPSmsSetup = mfaInfo.factors.alreadySetup.includes("otp-sms"); + let next = mfaInfo.factors.next; + + let factorsAllowedToBeSetup = mfaInfo.factors.allowedToSetup; + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("Illegal function call: For first factor setup, you do not need to call this function") + } +} +``` + + + + + +```tsx +import supertokensMultiFactorAuth from "supertokens-web-js-script/recipe/multifactorauth" +import supertokensSession from "supertokens-web-js-script/recipe/session" +async function fetchMFAInfo() { + if (await supertokensSession.doesSessionExist()) { + try { + let mfaInfo = await supertokensMultiFactorAuth.resyncSessionAndFetchMFAInfo() + let factorEmails = mfaInfo.emails; + let factorPhoneNumbers = mfaInfo.phoneNumbers; + + let emailsForOTPEmail = factorEmails["otp-email"]; + let phoneNumbersForOTPSms = factorEmails["otp-sms"]; + let isTotpSetup = mfaInfo.factors.alreadySetup.includes("totp"); + let isOTPEmailSetup = mfaInfo.factors.alreadySetup.includes("otp-email"); + let isOTPSmsSetup = mfaInfo.factors.alreadySetup.includes("otp-sms"); + let next = mfaInfo.factors.next; + + let factorsAllowedToBeSetup = mfaInfo.factors.allowedToSetup; + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("Illegal function call: For first factor setup, you do not need to call this function") + } +} +``` + + + + + +- In the above code snippet, we fetch the list of factors which the user must complete next (in the `next` array) and also all the relevant information to know what state each factor is in so that we can decide if we should ask the user to setup the factor (for example create a new TOTP device), or just ask them to solve the auth challenge instead (for example, showing the enter TOTP screen). +- The function is called `resyncSessionAndFetchMFAInfo` because it does two things: + - fetches the MFA info that you can consume to know the `next` array and what state each factor is in. + - resyncs the value of the `v` and `c` in the session's MFA claim. + + + + + + + + +Call the following API when you want to know the status of any factor. Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): + +```bash +curl --location --request PUT '^{form_apiDomain}^{form_apiBasePath}/mfa/info' \ +--header 'rid: multifactorauth' \ +--header 'Authorization: Bearer ...' +``` + + + +- In the above code snippet, we fetch the list of factors which the user must complete next (in the `next` array) and also all the relevant information to know what state each factor is in so that we can decide if we should ask the user to setup the factor (for example create a new TOTP device), or just ask them to solve the auth challenge instead (for example, showing the enter TOTP screen). +- The API is `PUT` and not `GET` because it does two things: + - fetches the MFA info that you can consume to know the `next` array and what state each factor is in. + - resyncs the value of the `v` and `c` in the session's MFA claim. + + + + + +- The struture of the raw JSON response is as follows: + ```json + { + "status": "OK", + "factors": { + "alreadySetup": ["totp", "otp-email", "..."], + "allowedToSetup": ["otp-sms", "otp-email", "..."], + "next": ["otp-sms", "..."] + }, + "emails": { + "otp-email": ["user1@example.com", "user2@example.com"], + "link-email": ["user1@example.com", "user2@example.com"], + }, + "phoneNumbers": { + "otp-sms": ["+1234567890", "+1098765432"], + "link-sms": ["+1234567890", "+1098765432"], + }, + } + ``` + + - `factors.alreadySetup` is an array that contains all factors that have been setup by the user. If the current factor is a part of this array, it means that you can directly take the user to the factor challenge screen. If your factor depends on an email or phone number (like in the case of `otp-sms` or `otp-email`), then you can find the email to send the code to in the `emails` or `phoneNumbers` object in the response with the key as the current factor ID. + - `factors.allowedToSetup` is an array that contains all factors that the user is allowed to setup at this point in time. This is not that useful during the sign in process, but may be useful post sign in if you want to know what are the factors that the user can setup at any point in time. + - `emails` is an object in which the key are all the factor IDs support by SuperTokens (and any custom factor ID added by you). The values against each of the keys is a list of emails that can be used to complete the factor. The first email (inddex 0) in the list is the preferred email to use for the factor. We calculate the order based on the first factor chosen by the user, and if the factor was already setup or not. + + If the array is empty, it means that there is no email associated with the user for that factor. This can happen only if the factor was not already setup. In this case, you should take the user to a screen to ask them to first enter an email, and then to the challenge screen. + + We will further go into this flow in our common flows guide later on. + - `phoneNumbers` is similar to the `emails` object, except that it contains phone numbers for factors that are based on phone numbers. + - The `factors.next` array determines the list of factors which the user must completed next. For example: + - If the next array is `["otp-email"]`, then we will show the user the enter OTP screen for the email associated with the first factor login. + - If the `n` array has multiple items: + - For the pre built UI, we will show the user a [factor chooser screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with. + - For custom UI, you would need to make this screen on your own. + - If the `next` is empty, it means that: + - There is a misconfig on the backend. This would show an access denied screen to the user. OR; + - There is another claim that needs to be finished first (like email verification), before the next MFA challenge can be shown. This can happen if you configure the backend's `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email is verified. + + + + + +### Frontend event hooks +The pre built UI emits a few events that you can listen to on the frontend. As an example, you can use these for analytics: + + + + + +```tsx +import SuperTokens from "supertokens-auth-react" +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" +import TOTP from "supertokens-auth-react/recipe/totp"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; + +SuperTokens.init({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + onHandleEvent: (context) => { + if (context.action === "PASSWORDLESS_CODE_SENT") { + // this event is fired when the user has successfully sent out an OTP email / SMS + } else if (context.action === "PASSWORDLESS_RESTART_FLOW") { + // This event is fired when the user's OTP has expired, or + // they have reached the max limit of number of failed OTP attempts. + } else if (context.action === "SUCCESS" && !context.createdNewSession) { + // this event is fired when successfully completing the OTP email / SMS challenge + // and if it's not used in first factor (cause we do !context.createdNewSession) + } + } + }), + TOTP.init({ + onHandleEvent: (context) => { + if (context.action === "TOTP_DEVICE_CREATED") { + // this event is fired during factor setup, when the user has successfully created the TOTP device. They still have to verify it by entering the TOTP. + } else if (context.action === "TOTP_DEVICE_VERIFIED") { + // this event is fired during factor setup, when the user has successfully verified the TOTP device + } else if (context.action === "TOTP_CODE_VERIFIED") { + // this event is fired when the user has successfully verified the TOTP code + // marking the TOTP factor as completed + } + } + }), + MultiFactorAuth.init({ + firstFactors: [/*...*/], + onHandleEvent: (context) => { + if (context.action === "FACTOR_CHOOSEN") { + let chosenFactorId = context.factorId; + // this event is fired when the user is shown the screen for + // picking one factor out of a choice of multiple factors + } + } + }) + ] +}) +``` + + + + + + + + + +### Handling support cases +There are some situations in which users may be locked out of their accounts and would need you to do certain steps to unlock their accounts. These cases are: + +#### ERR_CODE_009 +- This can happen when the second factor is `emailpassword`: + - API Path is `/signin POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_009)" + } + ``` +- This can happen if the email password account you are trying to do MFA with is not verified. + +#### ERR_CODE_010 +- This can happen when the second factor is `emailpassword`: + - API Path is `/signin POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_010)" + } + ``` +- This can happen if the email password account you are trying to do MFA with is already linked to another primary user that is not equal to the session user. + +#### ERR_CODE_011 +- This can happen when the second factor is `emailpassword`: + - API Path is `/signin POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_011)" + } + ``` +- This can happen if the email password account you are trying to do MFA cannot be linked to the session user because there already exists another primary user with the same email. + +#### ERR_CODE_012 +- This can happen when the second factor is `emailpassword`: + - API Path is `/signin POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_012)" + } + ``` +- In order to link the email password user with the session user, we need to make sure that the session user is a primary user. However, that can fail if there exists another primary user with the same email as the session user, and in this case, this error is returned to the frontend. + +#### ERR_CODE_013 +- This can happen when the second factor is `emailpassword`: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_013)" + } + ``` +- An example scenario of when in the following scenario: + - A user signs up with their phone number and OTP + - Post sign up, they are asked to add their email and a password for the account. In this case, since the entered email is not verified, this error will be shown. + +- To resolve this, we recommend that you change the flow to first ask the user to go through the email OTP flow post the first factor sign up, and then add a password to the account. This way, the email will be verified. + +#### ERR_CODE_014 +- This can happen when the second factor is `emailpassword`: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" + } + ``` +- An example scenario of when in the following scenario: + - Let's say that the app is confgured to not have autmatic account linking during the first factor. + - A user creates an email password account with email `e1`, verifies it, and links social login account to it with email `e2`. + - The user logs out, and then creates a social login account with email `e1`. Then, they are asked to add a password to this account. Since an email password account with `e1` already exists, SuperTokens will try and link that to this new account, but fail, since the email password account with `e1` is already a primary user. + +- To resolve this, we recommend that manually link the `e1` social login account with the `e1` email password account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. + +#### ERR_CODE_015 +- This can happen when the second factor is `emailpassword`: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" + } + ``` +- An example scenario of when in the following scenario: + - A user creates a social login account with email `e1` which becomes a primary user. + - The user logs out, and creates another social login account with email `e2`, which also becomes a primary user. + - The user is asked to add a password for the new account with an option to also specify an email with it (this is strange, but theoritically possible). They now enter the email `e1` for the email password account. + - This will cause this type of error since the linking of the new social logn and email account will fail since there already exists another primary user with the same (`e1`) email. + +- To resolve this, we recommend not allowing users to specify an email when asking them to add a password for their account. + +#### ERR_CODE_016 +- This can happen when the second factor is `emailpassword`: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" + } + ``` +- An example scenario of when in the following scenario: + - Let's say that the app is configured to not have automatic account linking during the first factor. + - A user signs up with a social login account using Google with email `e1`, and they add another social account, with Facebook, with the same email. + - The user logs out and creates another social login account with email `e1` (say Github), and then tries and adds a password to this account with email `e1`. Here, SuperTokens will try and make the Github login a primary user, but fail, since the email `e1` is already a primary user (with Google login). + +- To resolve this, we recommend that manually link the `e1` Github social login account with the `e1` Google social login account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. + +#### ERR_CODE_017 +- This can happen when the second factor is based on the passwordless recipe. + - API Path is `/signinup/code/consume POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_017)" + } + ``` +- This can happen when the passwordless account is trying to be linked to the account of the first factor, but it can't because the passwordless account is already linked with another primary user. + +#### ERR_CODE_018 +- This can happen when the second factor is based on the passwordless recipe. + - API Path is `/signinup/code/consume POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_018)" + } + ``` +- This can happen when the passwordless account is trying to be linked to the account of the first factor, but it can't because there exists another primary user with the same email as the passwordless account. + +#### ERR_CODE_019 +- This can happen when the second factor is based on the passwordless recipe. + - API Path is `/signinup/code POST` or `/signinup/code/consume POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_019)" + } + ``` +- This can happen when the passwordless account is trying to be linked to the account of the first factor, but, the first factor account cannot become a primary user because there exists another account with the same email as the first factor user account which is already primary. \ No newline at end of file diff --git a/v2/mfa/important-concepts.mdx b/v2/mfa/important-concepts.mdx new file mode 100644 index 000000000..ec61caaba --- /dev/null +++ b/v2/mfa/important-concepts.mdx @@ -0,0 +1,65 @@ +--- +id: important-concepts +title: Important concepts +hide_title: true +--- + +import MFAPaidBanner from '../community/reusableMD/mfa/MFAPaidBanner.mdx' + + + +# Important concepts + +## Auth factor IDs +Each auth challenge has a factor ID in SuperTokens: +- Email password auth: `emailpassword` +- Social login / enterprise SSO auth: `thirdparty` +- Passwordless: + - With email OTP: `otp-email` + - With SMS OTP: `sms-otp` + - With email magic link: `link-email` + - With SMS magic link: `sms-link` +- TOTP: `totp` + +These factor IDs will be used to configure the MFA requirements for users (except the `acccess-denied` one), and also will be used to indicate which auth challenges have been completed in the current session. + +## How are auth factors marked as completed? +We indicate the status of which auth factors are completed and if there are any pending via the session's access token payload. We store the following claim structre in the access token for this: + +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + }, + "v": false + } +} +``` +- `c` stands for `completed`, and `v` stands for `value`. +- The above payload indicates that the `emailpassword` auth factor has been completed, and that there are still more factors left before the user has finished sign up / login. +- The number against the `emailpassword` factor is the timestamp (in seconds since epoch) at which the factor was completed. +- If `v` is `true`, then it means that the user has completed all the factors required for sign up / login. This may not include the email verification requirement since that is a different factor. + +Each time an auth factor is completed, the SuperTokens backend adds it to the `c` (completed) object, and then re-evaluates the `v` boolean based on the MFA requirements for the user. This is done by calling the `getMFARequirementsForAuth` function from the MFA recipe (on the backend), which you can override to return the factors required for the user. In this guide, we will show you how to override this function to achieve different forms of MFA experiences. + +## First vs additional / secondary auth factors +There is a clear distinction that is made between first and additional factors in the SDK. When calling `MFA.init` on the backend, you need to provide a list of allowed first factors (or if using multi tenancy, you can configure the first factors on a per tenant basis and leave the `init` array empty). + +The first factors are those that are allowed to create a new session, whereas any other factor will only be allowed to modify an existing session. In case the user calls an additional factor's API without a session, the API will respond with a `401` error. + +## Relation of account linking and MFA +Account linking is when you like multiple login methods to the same user account. The individual login methods create their own "recipe user", and each of recipe user is linked to another recipe user to create one primary user. + +Theoritically, one can link any recipe user to another (i.e. there is no need for them to have the same email / phone number), however, for first factor automatic linking, we only link login methods if they are verified and have the same email. + +From an MFA point of view, whenever the user sets up a new passwordless factor (otp-email or otp-sms), this will create a new recipe user for the passwordless recipe, and will auto link it to the existing session's recipe user. + +Therefore, it is necessary to enable account linking for MFA to work. That being said, in the MFA guide, we do not enable first factor account linking, but you can enable that by following the automatic account linking guide in other parts of the docs. + +## Limitations of MFA +Magic link via email / SMS is only supported as a first factor for pre built UI. The reason we don't support it as a second factor is that if the magic link is opened on a different device, there would be no reference to the existing session (which was created post first factor completion). + +Instead, we support OTP based auth via email / SMS which achieves the same level of security as a magic link. + +Magic link is different form email verification links. Email verification links are supported in MFA flow since they do not create a new session on the browser in which they are opened. \ No newline at end of file diff --git a/v2/mfa/introduction.mdx b/v2/mfa/introduction.mdx index ac8b1a487..63dd26d1f 100644 --- a/v2/mfa/introduction.mdx +++ b/v2/mfa/introduction.mdx @@ -4,23 +4,30 @@ title: Introduction hide_title: true --- -import Disclaimer from "./pre-built-ui/disclaimer.mdx" +import MFAPaidBanner from '../community/reusableMD/mfa/MFAPaidBanner.mdx' -# Introduction + - +# Introduction ## Features -- Add Email or SMS based OTP / magic link as a second factor auth. +- Add Email or SMS based OTP as an auth factor. +- Add TOTP (Google authenticator app) as an auth factor. - Allow access to APIs only if multi factor auth has been completed. - Allow access to frontend routes only if multi factor auth has been completed. - Use your custom UI or our pre built UI for the second factor auth. +- Allow individual users to choose if they want to enable multi factor auth or not. +- Step up authentication +- Separate MFA config per tenant +- You can customise the MFA recipe to also add: + - Account recovery using backup codes + - Skipping MFA for trusted devices for a period of time. ## Demo application -See our [example app on GitHub](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-thirdpartyemailpassword-2fa-passwordless) that uses NodeJS and React. It has email password + social login (thirdpartyemailpassword recipe) as the first factor, and SMS OTP (passwordless recipe) as the second factor +You can download our example app that uses our pre built UI using the following command: + +```bash +npx create-supertokens-app@latest --recipe=multifactorauth +``` -## New features in progress (not released yet) -- Optionally enable multi factor for users (based on their choice). -- Multi factor using TOTP (Google authenticator app). -- Backup recovery codes. -- Step up auth \ No newline at end of file +It showcases MFA wherein the user is asked to pick one of TOTP, email OTP or phone OTP during sign up to be the secondary factor, and sets that up for them. On every subsequent sign in, then will be asked to complete the MFA challenge based on their choice. \ No newline at end of file diff --git a/v2/mfa/legacy-method/about.mdx b/v2/mfa/legacy-method/about.mdx new file mode 100644 index 000000000..859e96add --- /dev/null +++ b/v2/mfa/legacy-method/about.mdx @@ -0,0 +1,19 @@ +--- +id: legacy-vs-new +title: Legacy vs New method +hide_title: true +--- + + +# Legacy vs New method + +The legacy method for MFA requires you to customise the auth recipes we have to add MFA on top of it. Since it does not use the MFA recipe we have, it's free. + +The limitations of using the legacy method are: +- It does not support TOTP, but only email / SMS OTP +- When the end user completes the email / SMS OTP factor, there will be a separate user that's created for that login method which is not linked to the first factor login method for that user. This means that you will see two SuperTokens users for every single end use that goes through the MFA flow. +- It requires several customisations on top of the basic auth setup, which adds scope for error. + +When should you use the legacy method? +- If you are not using our Node backend SDK, then you have to use this, since we do not yet support the MFA recipe for non Node SDKs. +- If you are price sensitive and do not want to pay for the MFA recipe then you can use this method. That being said, if you are using our managed service, then depending on your end user's login pattern, the number of MAUs you will be charged for is double (assuming all users complete 2FA each month) than the actual MAU count. This in turn is more expensive than using the MFA recipe. \ No newline at end of file diff --git a/v2/mfa/backend/first-factor.mdx b/v2/mfa/legacy-method/backend/first-factor.mdx similarity index 98% rename from v2/mfa/backend/first-factor.mdx rename to v2/mfa/legacy-method/backend/first-factor.mdx index 452759e92..fd01a6e83 100644 --- a/v2/mfa/backend/first-factor.mdx +++ b/v2/mfa/legacy-method/backend/first-factor.mdx @@ -12,11 +12,13 @@ import PythonFrameworkSubTabs from "/src/components/tabs/PythonFrameworkSubTabs" import BackendSDKCasing from "/src/components/BackendSDKCasing" import AppInfoForm from "/src/components/appInfoForm" import CoreInjector from "/src/components/coreInjector" -import Disclaimer from "../pre-built-ui/disclaimer.mdx" + +:::caution +This is the legacy method of implementing MFA. It has several [disadvantages](../legacy-vs-new) compared to using our MFA recipe. +::: # Setting up the 1st factor - ## 1) Initialisation @@ -399,7 +401,7 @@ Session.init({ accessTokenPayload: { ...input.accessTokenPayload, // highlight-next-line - ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)), + ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)), }, }); }, diff --git a/v2/mfa/backend/protecting-api.mdx b/v2/mfa/legacy-method/backend/protecting-api.mdx similarity index 99% rename from v2/mfa/backend/protecting-api.mdx rename to v2/mfa/legacy-method/backend/protecting-api.mdx index 2807ecfa7..54a7681f8 100644 --- a/v2/mfa/backend/protecting-api.mdx +++ b/v2/mfa/legacy-method/backend/protecting-api.mdx @@ -9,11 +9,13 @@ import TabItem from '@theme/TabItem'; import NodeJSFrameworkSubTabs from "/src/components/tabs/NodeJSFrameworkSubTabs"; import GoFrameworkSubTabs from "/src/components/tabs/GoFrameworkSubTabs" import PythonFrameworkSubTabs from "/src/components/tabs/PythonFrameworkSubTabs" -import Disclaimer from "../pre-built-ui/disclaimer.mdx" + +:::caution +This is the legacy method of implementing MFA. It has several [disadvantages](../legacy-vs-new) compared to using our MFA recipe. +::: # Protecting API routes - In the previous steps, we saw the a session is created after the first factor, with `SecondFactorClaim` set to false, and then after the second factor is completed, we update that value to true. diff --git a/v2/mfa/backend/second-factor.mdx b/v2/mfa/legacy-method/backend/second-factor.mdx similarity index 99% rename from v2/mfa/backend/second-factor.mdx rename to v2/mfa/legacy-method/backend/second-factor.mdx index af6024a59..69b2a1bdc 100644 --- a/v2/mfa/backend/second-factor.mdx +++ b/v2/mfa/legacy-method/backend/second-factor.mdx @@ -12,11 +12,13 @@ import PythonFrameworkSubTabs from "/src/components/tabs/PythonFrameworkSubTabs" import BackendSDKCasing from "/src/components/BackendSDKCasing" import AppInfoForm from "/src/components/appInfoForm" import CoreInjector from "/src/components/coreInjector" -import Disclaimer from "../pre-built-ui/disclaimer.mdx" + +:::caution +This is the legacy method of implementing MFA. It has several [disadvantages](../legacy-vs-new) compared to using our MFA recipe. +::: # Setting up the 2nd factor - ## 1) Initialisation @@ -688,7 +690,7 @@ Session.init({ ...input, accessTokenPayload: { ...input.accessTokenPayload, - ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)), + ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)), }, }); }, @@ -1230,7 +1232,7 @@ Session.init({ ...input, accessTokenPayload: { ...input.accessTokenPayload, - ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)), + ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)), // highlight-next-line phoneNumber, }, diff --git a/v2/mfa/frontend-custom.mdx b/v2/mfa/legacy-method/frontend-custom.mdx similarity index 98% rename from v2/mfa/frontend-custom.mdx rename to v2/mfa/legacy-method/frontend-custom.mdx index 2219f8d29..ff7155c61 100644 --- a/v2/mfa/frontend-custom.mdx +++ b/v2/mfa/legacy-method/frontend-custom.mdx @@ -4,14 +4,15 @@ title: "Using a custom UI" hide_title: true --- -import Disclaimer from "./pre-built-ui/disclaimer.mdx" import AppInfoForm from "/src/components/appInfoForm" import NpmOrScriptTabs from "/src/components/tabs/NpmOrScriptTabs" import TabItem from '@theme/TabItem'; -# Using a custom UI +:::caution +This is the legacy method of implementing MFA. It has several [disadvantages](./legacy-vs-new) compared to using our MFA recipe. +::: - +# Using a custom UI ## 1) First factor recipe init diff --git a/v2/mfa/how-it-works.mdx b/v2/mfa/legacy-method/how-it-works.mdx similarity index 94% rename from v2/mfa/how-it-works.mdx rename to v2/mfa/legacy-method/how-it-works.mdx index 241bb7f43..7feb6501a 100644 --- a/v2/mfa/how-it-works.mdx +++ b/v2/mfa/legacy-method/how-it-works.mdx @@ -4,11 +4,12 @@ title: "How it works" hide_title: true --- -import Disclaimer from "./pre-built-ui/disclaimer.mdx" +:::caution +This is the legacy method of implementing MFA. It has several [disadvantages](./legacy-vs-new) compared to using our MFA recipe. +::: -# How it works - +# How it works You need to start by choosing your first factor auth. This can be any of the [auth recipes](https://supertokens.com/docs/guides) we support. A common choice is the [thirdpartyemailpassword recipe](https://supertokens.com/docs/thirdpartyemailpassword/introduction) which allows users to sign in with social or email / password login. diff --git a/v2/mfa/pre-built-ui/init.mdx b/v2/mfa/legacy-method/pre-built-ui/init.mdx similarity index 93% rename from v2/mfa/pre-built-ui/init.mdx rename to v2/mfa/legacy-method/pre-built-ui/init.mdx index b7edf1678..bf26db263 100644 --- a/v2/mfa/pre-built-ui/init.mdx +++ b/v2/mfa/legacy-method/pre-built-ui/init.mdx @@ -7,11 +7,12 @@ hide_title: true import FrontendSDKTabs from "/src/components/tabs/FrontendSDKTabs" import TabItem from '@theme/TabItem'; import AppInfoForm from "/src/components/appInfoForm" -import Disclaimer from "./disclaimer.mdx" -# 1) Recipe init +:::caution +This is the legacy method of implementing MFA. It has several [disadvantages](../legacy-vs-new) compared to using our MFA recipe. +::: - +# 1) Recipe init To start, we want to initialise the [ThirdPartyEmailPassword](https://supertokens.com/docs/thirdpartyemailpassword/quick-setup/frontend) and the [Passwordless](https://supertokens.com/docs/passwordless/quick-setup/frontend) recipes as mentioned in their quick setup section: diff --git a/v2/mfa/pre-built-ui/protecting-routes.mdx b/v2/mfa/legacy-method/pre-built-ui/protecting-routes.mdx similarity index 93% rename from v2/mfa/pre-built-ui/protecting-routes.mdx rename to v2/mfa/legacy-method/pre-built-ui/protecting-routes.mdx index 84a203711..cd4848093 100644 --- a/v2/mfa/pre-built-ui/protecting-routes.mdx +++ b/v2/mfa/legacy-method/pre-built-ui/protecting-routes.mdx @@ -7,11 +7,12 @@ hide_title: true import FrontendSDKTabs from "/src/components/tabs/FrontendSDKTabs" import TabItem from '@theme/TabItem'; import AppInfoForm from "/src/components/appInfoForm" -import Disclaimer from "./disclaimer.mdx" -# 3) Protecting routes +:::caution +This is the legacy method of implementing MFA. It has several [disadvantages](../legacy-vs-new) compared to using our MFA recipe. +::: - +# 3) Protecting routes Now we can wrap your application routes with the `SessionAuth` component, which should check for MFA completion by default: diff --git a/v2/mfa/pre-built-ui/showing-login-ui.mdx b/v2/mfa/legacy-method/pre-built-ui/showing-login-ui.mdx similarity index 98% rename from v2/mfa/pre-built-ui/showing-login-ui.mdx rename to v2/mfa/legacy-method/pre-built-ui/showing-login-ui.mdx index 2ac34311a..ee1f35164 100644 --- a/v2/mfa/pre-built-ui/showing-login-ui.mdx +++ b/v2/mfa/legacy-method/pre-built-ui/showing-login-ui.mdx @@ -7,11 +7,12 @@ hide_title: true import FrontendSDKTabs from "/src/components/tabs/FrontendSDKTabs" import TabItem from '@theme/TabItem'; import AppInfoForm from "/src/components/appInfoForm" -import Disclaimer from "./disclaimer.mdx" -# 2) Showing the first and second factor UI +:::caution +This is the legacy method of implementing MFA. It has several [disadvantages](../legacy-vs-new) compared to using our MFA recipe. +::: - +# 2) Showing the first and second factor UI + +# Migration from legacy MFA to new MFA method + +This page only applies if you have used our [legacy MFA method](./legacy-method/how-it-works) and want to migrate to the new MFA method. + +From a code point of view, [here is the diff](https://github.com/supertokens/supertokens-auth-react/commit/f96e11a527ccd9df71c25798c316dd3de770dde1) of all changes required: +- We no longer need the custom `secondFactoeClaim` since the MFA recipe adds its [own MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed). +- On the backend, we initialise the MFA recipe: + - In the example app, we use email password and social login as the first factor, so the we set `firstFactors` variable to to `[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY]`. + - We also set the required second factor to return OTP phone, but you can set it to OTP email as well. + - We override the `resyncSessionAndFetchMFAInfoPUT` API to sync old session claim with the new one. This API is called from the frontend during sign in, and also whenever you use `` (for the pre built UI) or call `await Session.validateClaims()` for your custom UI. + - We remove the custom overrides for Passwordless.init and for Session.init since those are now handled by the new MFA recipe. + - We initialise the AccountLinking recipe with the config that enables account linking for first and second factors. + - A side effect of using our multi factor auth is that if you have enabled the email verification recipe, we will ask users to complete email verification before completing any secondary factors. +- On the frontend for pre built UI: + - You will no longer need to use the custom SecondFactorClaim, and also no longer need to create a SecondFactor component (since we provide a pre built one). + - We initialise the MultiFactorAuth recipe, and remove the Session.init override from before asl well. +- On the frontend for custom UI: + - You no longer will need to use the custom SecondFactorClaim and instead, you can see our guide for email / SMS OTP in the above sections. + +## Understanding the override code for `resyncSessionAndFetchMFAInfoPUT` +We start by getting the user object from of the session's user ID along with the user's metadata. If the user object does not already contain a passwordless login method, it means we have not linked the passwordless user yet. If also, the metadata has the `passwordlessUserId` prop in it, it means that this user had previously setup email / SMS OTP as a second factor. In this case, we now attempt to link the passwordless user to the session user. + +For linking, we need to first ensure that the session user is a primary user, and if not, we call the `createPrimaryUser` function from the account linking recipe. After that, we call the `linkAccounts` function with the session user id and the passwordless user ID (pulled from the metadata). We are now in a state where the session user has two login methods: +- The first factor login method +- The passwordless login method for 2FA + +Finally, we update the session claim value to contain the MFA claim and also mark the `OTP_PHONE` factor as completed (you can use `OTP_EMAIL`). This will prevent users from being asked to complete the second factor again if they have already done so in the current session. \ No newline at end of file diff --git a/v2/mfa/migration.mdx b/v2/mfa/migration.mdx new file mode 100644 index 000000000..3d4b6d22a --- /dev/null +++ b/v2/mfa/migration.mdx @@ -0,0 +1,178 @@ +--- +id: migration +title: Migration to SuperTokens from another auth solution +hide_title: true +--- + +import MFAPaidBanner from '../community/reusableMD/mfa/MFAPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; +import TabItem from '@theme/TabItem'; +import PythonSyncAsyncSubTabs from "/src/components/tabs/PythonSyncAsyncSubTabs"; +import CoreInjector from "/src/components/coreInjector" +import CustomAdmonition from "/src/components/customAdmonition" + + + +# Migration to SuperTokens from another auth solution + +This section covers migration of secondary factors from your existing auth solution to SuperTokens. To see how to migrate the first factor, refer to our migration docs for the recipe you have setup as the first factor: + +- [EmailPassword](/docs/emailpassword/migration/about) +- [ThirdParty](/docs/thirdparty/migration/about) +- [Passwordless](/docs/passwordless/migration/about) +- [ThirdPartyEmailPassword](/docs/thirdpartyemailpassword/migration/about) +- [ThirdPartyPasswordless](/docs/thirdpartypasswordless/migration/about) + +After you have completed the first factor migration, you can focus on the secondary factor migration for the user. We will cover migration steps for each of the support secondary factors we have. + +:::important +Before reading further, make sure you have understood how MFA works with SuperTokens based on the docs above in this guide. +::: + +## TOTP +From the migration step above (for the first factor), you should have the user's user ID generated by SuperTokens. Call the following API to create a new TOTP device for the user with their user ID and extisting TOTP secret: + + + +```bash +curl --location --request POST '^{coreInjector_uri_without_quotes}/recipe/totp/device/import' \ +--header 'api-key: ^{coreInjector_api_key_without_quotes}' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "userId": "", + "skew": 1, + "period": 30, + "secretKey": "...." +}' +``` + + + +The above API call will create a new TOTP device for the user. The `secretKey` field should be fetched from your existing auth provider (we expect it to be in base32 format). You can also provide an optional `deviceName` as a prop in the body which will help identify the TOTP device for future operations like if you want to delete this device. If you do not provide a `deviceName`, SuperTokens will generate a device name for this user like `TOTP Device X`, where `X` is 1, 2, 3 and so on depending on how many devices this user already has. + +:::caution +- If you have mapped the SuperTokens user ID to an external user ID, then you should use the external user ID in the above API call instead of the SuperTokens user ID. +- The `secretKey` field must be `base32` encoded. If your existing auth provider provides the key encoded in any other format, you must decode their key and then encode it in base32 format. For example, if the provided key is in base64 format, then you must do `base32encode(base64decode(secretKeyFromAuthProvider))` +::: + +## Email / SMS OTP + +SuperTokens relies on the passwordless recipe to complete this type of factor. The overall process here is to: +- Create a new passwordless user with the email of the first factor, or with a phone number. +- Make the first factor user a primary user for account linking purposes. +- Link the passwordless user to the first factor user. + + + +### Generate passwordless code + +**With Email** + +```bash +curl --location --request POST '^{coreInjector_uri_without_quotes}/recipe/signinup/code' \ +--header 'api-key: ^{coreInjector_api_key_without_quotes}' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "email": "johndoe@example.com" +}' +``` + +**With Phone Number** + +```bash + +curl --location --request POST '^{coreInjector_uri_without_quotes}/recipe/signinup/code' \ +--header 'api-key: ^{coreInjector_api_key_without_quotes}' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "phoneNumber": "+14155552671" +}' +``` + +On successfully generating the passwordless code you should see the following response + +```json +{ + "status": "OK", + "preAuthSessionId": "d3Zpa9eoyV2Wr7uN5DLr6H1clzbwwGTc_0wIIXJT55M=", + "codeId": "4fe93f8e-a5da-4588-82e2-314c6993b345", + "deviceId": "+cWm1Y2EFxEPyHM7CAwYyAdkakBeoEDm6IOGT3xfa1U=", + "userInputCode": "463152", + "linkCode": "UlEb3-gbIYow61ce6RNzghkGN8qcHkpRwbhHbvMEjxY=", + "timeCreated": 1664283193059, + "codeLifetime": 900000 +} +``` + +### Consume the passwordless code to create the passwordless user + +Retrieve the `preAuthSessionId` and `linkCode` from the previous response and set them as request body parameters for the consume code request. + +```bash +curl --location --request POST '^{coreInjector_uri_without_quotes}/recipe/signinup/code/consume' \ +--header 'api-key: ^{coreInjector_api_key_without_quotes}' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "preAuthSessionId": "d3Zpa9eoyV2Wr7uN5DLr6H1clzbwwGTc_0wIIXJT55M=", + "linkCode": "UlEb3-gbIYow61ce6RNzghkGN8qcHkpRwbhHbvMEjxY=" +}' +``` + +If the user has both email and password associated with them, then you can call the update user API to associate the missing information + +```bash +curl --location --request PUT '^{coreInjector_uri_without_quotes}/recipe/user' \ +--header 'api-key: ^{coreInjector_api_key_without_quotes}' \ +--header 'rid: passwordless' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "userId": "fa7a0841-b533-4478-95533-0fde890c3483", + "email": "johndoe@gmail.com", + "phoneNumber": "+14155552671" +}' +``` + + + +### Make the first factor user a primary user +The user ID returned from the first factor migration should be made a primary user with the following API call: + + + +```bash +curl --location --request POST '^{coreInjector_uri_without_quotes}/recipe/accountlinking/user/primary' \ +--header 'api-key: ^{coreInjector_api_key_without_quotes}' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "recipeUserId": "" +}' +``` + + + +This will allow another recipe user to be linked to this user. + +:::caution +If you have mapped the first factor's SuperTokens user ID to an external user ID, then you should use the external user ID in the above API call (for the `recipeUserId` field) instead of the SuperTokens user ID. +::: + +### Link the passwordless user to the first factor user +Then we link the newly created passwordless user to the first factor user with the following API call: + + + +```bash +curl --location --request POST '^{coreInjector_uri_without_quotes}/recipe/accountlinking/user/link' \ +--header 'api-key: ^{coreInjector_api_key_without_quotes}' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "primaryUserId": "", + "recipeUserId": "" +}' +``` + + + +:::caution +If you have mapped the first factor's SuperTokens user ID to an external user ID, then you should use the external user ID in the above API call (for the `primaryUserId` field) instead of the SuperTokens user ID. +::: diff --git a/v2/mfa/old-sdk-to-new.mdx b/v2/mfa/old-sdk-to-new.mdx new file mode 100644 index 000000000..28212446e --- /dev/null +++ b/v2/mfa/old-sdk-to-new.mdx @@ -0,0 +1,120 @@ +--- +id: old-sdk-to-new +title: Migration from an older SuperTokens SDK to a newer one +hide_title: true +--- + +import MFAPaidBanner from '../community/reusableMD/mfa/MFAPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; +import TabItem from '@theme/TabItem'; +import PythonSyncAsyncSubTabs from "/src/components/tabs/PythonSyncAsyncSubTabs"; +import CoreInjector from "/src/components/coreInjector" +import CustomAdmonition from "/src/components/customAdmonition" + + + +# Migration from an older SuperTokens SDK to a newer one + +This section is applicable to those who want to enable MFA for the first time and are already using SuperTokens in production. The following are the steps you need to take: +- Make sure that you have updated your SuperTokens core, backend SDK and frontend SDK that supports MFA (you can find these versions in the CHANGELOG.md files in their respective github repository). Make sure to read the migration guide for each of the breaking version upgrades. You should aim to get your existing feature set working with the new version of SuperTokens before you enable MFA. +- Follow the backend and frontend setup we have in this guide along with the factor specific setup (TOTP or email / sms OTP). + + +## If enabling MFA for all users at once + +If you have enabled MFA for all users, then existing logged in users will asked to complete the secondary factor as soon as they visit your app / website once you have pushed the changes to production. This happens because their existing session is modified to add the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) into it, however, the `v` value in the claim will be `false` since they have not completed MFA yet. This would fail the validators on the frontend which would redirect the user to the MFA login screen. + +If you have clients like mobile apps, which take time to upgrade across your entire user base, then you may want to remove the global MFA validator on the backend and make it run only when you know that the request is coming from an updated client: + + + + +```tsx +import Session from "supertokens-node/recipe/session"; +import { ClaimValidationResult } from "supertokens-node/recipe/session/types"; +import SuperTokens, { getRequestFromUserContext } from "supertokens-node"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { UserContext } from "supertokens-node/types" + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // other recipes.. + Session.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // highlight-start + getGlobalClaimValidators: (input) => { + // We remove the existing default MFA validator which checks that + // the client has finished MFA. + let newValidatorsArray = input.claimValidatorsAddedByOtherRecipes.filter(v => v.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + + // We create an instance of the default validator that is added + // by SuperTokens so that we can make modificationts to it + let originalValidator = MultiFactorAuth.MultiFactorAuthClaim.validators.hasCompletedMFARequirementsForAuth(); + + // We create a custom validator based on the default validator + let customValidator = { + ...originalValidator, + validate: async (payload: any, userContext: UserContext): Promise => { + // We only want to run the validation check + // if we know that the client is not an older one. + // In this examlpe, we do this based on the header + // in the API request, but the logic here can be + // anything that you like. + let request = getRequestFromUserContext(userContext); + if (request !== undefined) { + let isOlderClient = request.getHeaderValue("clilent-version") !== "2.0"; + if (isOlderClient) { + // we return true early for older clients. + return { + isValid: true + } + } + } + + // for newer clients, we call the original validate function + // which will check the claim value in the session. + return originalValidator.validate(payload, userContext); + } + } + + return [customValidator, ...newValidatorsArray]; + } + // highlight-end + } + } + } + }) + ] +}) +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +The code above will still result in the MFA claim being added to all sessions, with the `v` boolean in it being `false`, however, for older clients, we will not run the validator that checks if the `v` value is true on the backend since those clients have no way to show the MFA UI without updating the app. + +:::caution +You want to add the above exception only for a certain amount of time until you force all users to update their mobile app. This is because the exception method can be used as a way for malicious users to bypass MFA by spoofing that they are using an older client. +::: \ No newline at end of file diff --git a/v2/mfa/pre-built-ui/disclaimer.mdx b/v2/mfa/pre-built-ui/disclaimer.mdx deleted file mode 100644 index 5ab964026..000000000 --- a/v2/mfa/pre-built-ui/disclaimer.mdx +++ /dev/null @@ -1,5 +0,0 @@ - -:::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 TOTP. Stay tuned. -- A demo app that uses the pre built UI can be found on [our GitHub](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-thirdpartyemailpassword-2fa-passwordless). -::: \ No newline at end of file diff --git a/v2/mfa/protect-routes.mdx b/v2/mfa/protect-routes.mdx new file mode 100644 index 000000000..464a4c312 --- /dev/null +++ b/v2/mfa/protect-routes.mdx @@ -0,0 +1,1105 @@ +--- +id: protect-routes +title: Protecting frontend and backend routes +hide_title: true +show_ui_switcher: true +--- + +import MFAPaidBanner from '../community/reusableMD/mfa/MFAPaidBanner.mdx' +import TabItem from '@theme/TabItem'; +import CustomAdmonition from "/src/components/customAdmonition" +import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" +import FrontendPreBuiltUITabs from "/src/components/tabs/FrontendPreBuiltUITabs" +import FrontendCustomUITabs from "/src/components/tabs/FrontendCustomUITabs" +import NpmOrScriptTabs from "/src/components/tabs/NpmOrScriptTabs" +import AppInfoForm from "/src/components/appInfoForm" +import FrontendMobileSubTabs from "/src/components/tabs/FrontendMobileSubTabs" +import {Question, Answer}from "/src/components/question" +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs" +import NodeJSFrameworkSubTabs from "/src/components/tabs/NodeJSFrameworkSubTabs"; + + + +# Protecting frontend and backend routes + +In thie section, we will talk about how to protect your frontend and backend routes to make them accessible only when the user has finished all the MFA challenges configured for them. + +In both the backend and the frontend, we will protect routes based on the value of [MFA claim in the session's access token payload](./important-concepts#how-are-auth-factors-marked-as-completed). + +## Protecting API routes + +### The default behaviour +When you call `MultiFactorAuth.init` in the `supertokens.init` on the backend, SuperTokens **automatically adds a session claim validator globally**. This validator checks that the value of `v` in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true` before allowing the request to proceed. If the value of `v` is `false`, the validator will send a 403 error to the frontend. + +:::important +This validator is added globally, which means that everytime you use `verifySession` or `getSession` from our backend SDKs, this check will happen. This means that you don't need to add any extra code on a per API level to enforce MFA. +::: + +### Excluding routes from the default check +If you wish to not have the default validator check in a certain backend route, you can exclude that when calling `verifySession` in the following way: + + + + + + +```tsx +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import express from "express"; +import { SessionRequest } from "supertokens-node/framework/express"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +let app = express(); + +app.post( + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + async (req: SessionRequest, res) => { + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators + } +); +``` + + + +```tsx +import Hapi from "@hapi/hapi"; +import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; +import {SessionRequest} from "supertokens-node/framework/hapi"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +let server = Hapi.server({ port: 8000 }); + +server.route({ + path: "/update-blog", + method: "post", + options: { + pre: [ + { + method: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators + } +}) +``` + + + +```tsx +import Fastify from "fastify"; +import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; +import { SessionRequest } from "supertokens-node/framework/fastify"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +let fastify = Fastify(); + +fastify.post("/update-blog", { + preHandler: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), +}, async (req: SessionRequest, res) => { + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators +}); +``` + + + + +```tsx +import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; +import { SessionEvent } from "supertokens-node/framework/awsLambda"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +async function updateBlog(awsEvent: SessionEvent) { + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators +}; + +exports.handler = verifySession(updateBlog, { + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, +}); +``` + + + + +```tsx +import KoaRouter from "koa-router"; +import { verifySession } from "supertokens-node/recipe/session/framework/koa"; +import {SessionContext} from "supertokens-node/framework/koa"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +let router = new KoaRouter(); + +router.post("/update-blog", verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), async (ctx: SessionContext, next) => { + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators +}); +``` + + + + +```tsx +import { inject, intercept } from "@loopback/core"; +import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; +import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; +import Session from "supertokens-node/recipe/session"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +class Example { + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } + @post("/update-blog") + @intercept(verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + })) + @response(200) + async handler() { + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators + } +} +``` + + + + +```tsx +import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import { SessionRequest } from "supertokens-node/framework/express"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +// highlight-start +export default async function example(req: SessionRequest, res: any) { + await superTokensNextWrapper( + async (next) => { + await verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + })(req, res, next); + }, + req, + res + ) + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators +} +``` + + + + +```tsx +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +// @ts-ignore +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators + return NextResponse.json({}) + }, + { + // highlight-start + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + // highlight-end + }); +} +``` + + + + +```tsx +import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; +import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; +// @ts-ignore +import { AuthGuard } from './auth/auth.guard'; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +@Controller() +export class ExampleController { + @Post('example') + @UseGuards(new AuthGuard({ + overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + })) + async postExample(@Session() session: SessionContainer): Promise { + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators + return true; + } +} +``` + + + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +The same modification can be done for `getSession` as well. + +### Manually checking the MFA claim value +If you want to have a more complex logic for doing authorisation based on the MFA claim (other than checking if `v` is `true`), you can do it in this way: + + + + + + +```tsx +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import express from "express"; +import { SessionRequest } from "supertokens-node/framework/express"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +let app = express(); + +app.post( + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + async (req: SessionRequest, res) => { + let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + } +); +``` + + + +```tsx +import Hapi from "@hapi/hapi"; +import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; +import {SessionRequest} from "supertokens-node/framework/hapi"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +let server = Hapi.server({ port: 8000 }); + +server.route({ + path: "/update-blog", + method: "post", + options: { + pre: [ + { + method: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + } +}) +``` + + + +```tsx +import Fastify from "fastify"; +import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; +import { SessionRequest } from "supertokens-node/framework/fastify"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +let fastify = Fastify(); + +fastify.post("/update-blog", { + preHandler: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), +}, async (req: SessionRequest, res) => { + let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } +}); +``` + + + + +```tsx +import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; +import { SessionEvent } from "supertokens-node/framework/awsLambda"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +async function updateBlog(awsEvent: SessionEvent) { + let mfaClaimValue = await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await awsEvent.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } +}; + +exports.handler = verifySession(updateBlog, { + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, +}); +``` + + + + +```tsx +import KoaRouter from "koa-router"; +import { verifySession } from "supertokens-node/recipe/session/framework/koa"; +import { SessionContext } from "supertokens-node/framework/koa"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +let router = new KoaRouter(); + +router.post("/update-blog", verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, +}), async (ctx: SessionContext, next) => { + let mfaClaimValue = await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await ctx.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } +}); +``` + + + + +```tsx +import { inject, intercept } from "@loopback/core"; +import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; +import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; +import Session from "supertokens-node/recipe/session"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +class Example { + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } + @post("/update-blog") + @intercept(verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + })) + @response(200) + async handler() { + let mfaClaimValue = await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await (this.ctx as any).session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + } +} +``` + + + + +```tsx +import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import { SessionRequest } from "supertokens-node/framework/express"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +// highlight-start +export default async function example(req: SessionRequest, res: any) { + await superTokensNextWrapper( + async (next) => { + await verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + })(req, res, next); + }, + req, + res + ) + let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + await superTokensNextWrapper( + async (next) => { + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + }, + req, + res + ) + } +} +``` + + + + +```tsx +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +// @ts-ignore +import { backendConfig } from "@/app/config/backend"; +import { Error as STError } from "supertokens-node/recipe/session" + +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + let mfaClaimValue = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + const error = new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + return NextResponse.json(error, { status: 403 }); + } + return NextResponse.json({}) + }, + { + // highlight-start + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + // highlight-end + }); +} +``` + + + + +```tsx +import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; +import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; +// @ts-ignore +import { AuthGuard } from './auth/auth.guard'; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +@Controller() +export class ExampleController { + @Post('example') + @UseGuards(new AuthGuard({ + overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + })) + async postExample(@Session() session: SessionContainer): Promise { + let mfaClaimValue = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + return true; + } +} +``` + + + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +- In the code snippet above, we remove the default validator that was added to the global validators (which checks if the `v` value in the claim is true or not). You don't need to do this, but in the code snippet above, we show it anyway. +- Then in the API logic, we manually fetch the claim value, and then check if TOTP has been completed or not. If it hasn't, we send back a 403 error to the frontend. + +You can use a similar approach as shown above to do any kind of check. + +### When using a JWT verification lib +If you are doing JWT verification manually, then post verification, you should check the payload of the JWT and make sure that the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true`. This would be equavalent to doing a check as our default claim validator mentioned above. + +:::important +Make sure to also do other checks on the JWT's payload. For example, if you require all users to have finished email verification, then we need to check for htat claim as well in the JWT. +::: + + + + + +## Protecting frontend routes + + + + + +### The default behaviour + +When you call `MultiFactorAuth.init` in the `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you use the `SessionAuth` component. This validator checks if the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true` or not. If it is not, then the user will be redirected to the MFA auth screen. + +### Other forms of authorization +If you do not want to run our default validator on a specific route, you can modify the use of `SessionAuth` in the following way: + +```tsx +import React from "react"; +import { SessionAuth, useSessionContext, useClaimValue } from 'supertokens-auth-react/recipe/session'; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; + +const VerifiedRoute = (props: React.PropsWithChildren) => { + return ( + { + return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.id); + }}> + + {props.children} + + + ); +} + +function InvalidClaimHandler(props: React.PropsWithChildren) { + + const claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + + if (claimValue.loading) { + return null; + } + + if (claimValue.value === undefined || !("totp" in claimValue.value.c)) { + return
      You do not have access to this page because you have not completed TOTP. Please click here to finish to proceed.
      + } + + // the user has finished TOTP, so we can render the children + return
      {props.children}
      ; +} +``` +- In the snippet above, we remove the default claim validator that is added to `SessionAuth`, and add out own logic that reads from the session's payload. +- Finally, we check if the user has completed TOTP or not. If not, we show a message to the user, and ask them to complete TOTP. Of course, if this is all you want to do, then the default validator already does that. But the above has the boilerplate for how you can do more complex checks. + +
      + + + +By default, when you do `MultiFactorAuth.init` in `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you call the `Session.validateClaims` function. This validator checks if the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true` or not. + +```tsx +import Session from "supertokens-web-js/recipe/session"; +import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; + +async function shouldLoadRoute(): Promise { + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims(); + + if (validationErrors.length === 0) { + // user has finished all MFA factors. + return true; + } else { + for (const err of validationErrors) { + if (err.validatorId === MultiFactorAuthClaim.id) { + // user has not finished MFA factors. + let mfaClaimValue = await Session.getClaimValue({ + claim: MultiFactorAuthClaim + }); + if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { + // the user has not finished totp + return false; + } + } + } + } + } + // a session does not exist, or email is not verified + return false +} +``` + +In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it will be reflected in the `validationErrors` variable. The `MultiFactorAuthClaim` validator will be automatically checked by this function since you have initialized the MFA recipe. + +In case the claim fails, you can get the claim value and check which factor is not completed. In the above code, we check that if it's the TOTP factor that is missing when the claim fails and return `false` from this function. However, it's really up to you for what you want to do next. For example, you could redirect the user to the TOTP factor screen. + + + + + +~COPY-TABS=angular + + + +
      + +
      + + + +## Protecting Frontend routes + + + + + +By default, when you do `MultiFactorAuth.init` in `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you call the `Session.validateClaims` function. This validator checks if the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true` or not. + + + + +```tsx +import Session from "supertokens-web-js/recipe/session"; +import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; + +async function shouldLoadRoute(): Promise { + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims(); + + if (validationErrors.length === 0) { + // user has finished all MFA factors. + return true; + } else { + for (const err of validationErrors) { + if (err.validatorId === MultiFactorAuthClaim.id) { + // user has not finished MFA factors. + let mfaClaimValue = await Session.getClaimValue({ + claim: MultiFactorAuthClaim + }); + if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { + // the user has not finished totp + return false; + } + } + } + } + } + // a session does not exist, or email is not verified + return false +} +``` + + + + +```tsx +import supertokensSession from "supertokens-web-js-script/recipe/session"; +import supertokensMultiFactorAuth from "supertokens-web-js-script/recipe/multifactorauth"; +async function shouldLoadRoute(): Promise { + if (await supertokensSession.doesSessionExist()) { + let validationErrors = await supertokensSession.validateClaims(); + + if (validationErrors.length === 0) { + // user has finished all MFA factors. + return true; + } else { + for (const err of validationErrors) { + if (err.validatorId === supertokensMultiFactorAuth.MultiFactorAuthClaim.id) { + // user has not finished MFA factors. + let mfaClaimValue = await supertokensSession.getClaimValue({ + claim: supertokensMultiFactorAuth.MultiFactorAuthClaim + }); + if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { + // the user has not finished totp + return false; + } + } + } + } + } + // a session does not exist, or email is not verified + return false +} +``` + + + + +In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it will be reflected in the `validationErrors` variable. The `MultiFactorAuthClaim` validator will be automatically checked by this function since you have initialized the MFA recipe. + +In case the claim fails, you can get the claim value and check which factor is not completed. In the above code, we check that if it's the TOTP factor that is missing when the claim fails and return `false` from this function. However, it's really up to you for what you want to do next. For example, you could redirect the user to the TOTP factor screen. + + + + + +In your app, you can check the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) values to know which factors have been completed by the user and take action based on that. + + + + + + + +```tsx +import SuperTokens from 'supertokens-react-native'; + +async function checkIfMFAIsCompleted() { + if (await SuperTokens.doesSessionExist()) { + + // highlight-start + let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; + + if (isMFACompleted) { + // All required factors for MFA have been completed + } else { + // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user + } + // highlight-end + } +} +``` + + + + + +```kotlin +import android.app.Application +import com.supertokens.session.SuperTokens +import org.json.JSONObject + +class MainApplication: Application() { + fun checkIfMFAIsCompleted() { + val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this); + val isMFACompleted: Boolean = (accessTokenPayload.get("st-mfa") as JSONObject).get("v") as Boolean + if (isMFACompleted) { + // All required factors for MFA have been completed + } else { + // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user + } + } +} +``` + + + + + +```swift +import UIKit +import SuperTokensIOS + +fileprivate class ViewController: UIViewController { + func checkIfMFAIsCompleted() { + if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely(), let mfaObject: [String: Any] = mfaObject["st-mfa"] as? [String: Any], let isMFACompleted: Bool = mfaObject["v"] as? Bool { + if isMFACompleted { + // All required factors for MFA have been completed + } else { + // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user + } + } + } +} +``` + + + + + +```dart +import 'package:supertokens_flutter/supertokens.dart'; + +Future checkIfMFAIsCompleted() async { + var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); + + if (accessTokenPayload.containsKey("st-mfa")) { + Map mfaObject = accessTokenPayload["st-mfa"]; + + if (mfaObject.containsKey("v")) { + bool isMFACompleted = mfaObject["v"]; + + if (isMFACompleted) { + // All required factors for MFA have been completed + } else { + // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user + } + } + } +} +``` + + + + + + + +If the MFA claim value is missing in the access token payload, then it means that the session was created before you enabled MFA on the backend. In this case, you can call the [MFA Info](./frontend-setup#mfa-info-endpoint) endpoint which will add the MFA claim to the session and check again. + + + + + + +
      diff --git a/v2/mfa/security.mdx b/v2/mfa/security.mdx new file mode 100644 index 000000000..c8ed70afb --- /dev/null +++ b/v2/mfa/security.mdx @@ -0,0 +1,24 @@ +--- +id: security +title: Security considerations +hide_title: true +show_ui_switcher: true +--- + +import MFAPaidBanner from '../community/reusableMD/mfa/MFAPaidBanner.mdx' + + + +# Security considerations + +SuperTokens enforces that a user has completed all the required factors by keeping track of and checking them in the [user's access token payload](./important-concepts#how-are-auth-factors-marked-as-completed). + +There are several checks done in our sign in / sign up APIs to ensure safety and prevent misuse: +- When a sign in or sign up API is called for a particula login method, and if the user doesn't already have a session, we enforce that the current login method is a part of the first factors configuration, else we return a 401 status code. +- If a user is required to complete a MFA challenge, for example TOTP, if they already have a verified TOTP device, they cannot setup any other factor before completing this factor challenge, and if they do not yet have a verfied TOTP device, then the only action they are allowed to take is to create a new TOTP device. This ensures that a user cannot bypass the MFA challenges of the current or future step. +- When a user creates a new TOTP device, it cannot be used unless they first verify it by entering the inital TOTP code. +- If the email of the 2nd factor login method is not verified, by default, we do not allow it to be setup or used as a 2nd factor, unless the session user has a login method that has the same email which is verified. +- There is a fixed number of times (5 times by default) a user can enter an invalid TOTP code, after which they have to wait for 15 mins before trying again. This timeout and the max attempts count can be changed in the core config. +- During sign up (not sign in), for email / SMS OTP challenge, the email / SMS that the OTP is sent to is determined by the frontend. This is intentional because it allows you to easily create a flow in which the email the OTP is sent to may not be the same as the login method of the first factor. However, from a security point of view, it allows a malicious actor to send an OTP to a different email / phone number than the first factor's phone or email. This is not an issue if you are using email OTP as a method for email verification because the email verification recipe checks that the email of the first factor is verified, and in the case of the malicious user, the email of the first factor won't be verified because they entered a different email for otp-email challenge. + + diff --git a/v2/mfa/sidebars.js b/v2/mfa/sidebars.js index d1bb5d4a0..8d65cec80 100644 --- a/v2/mfa/sidebars.js +++ b/v2/mfa/sidebars.js @@ -1,25 +1,71 @@ module.exports = { sidebar: [ "introduction", - "how-it-works", + "important-concepts", + "backend-setup", + "frontend-setup", { type: 'category', - label: 'Backend Setup', - collapsed: false, + label: 'TOTP', + collapsed: true, items: [ - "backend/first-factor", - "backend/second-factor", - "backend/protecting-api", + "totp/totp-for-all-users", + "totp/totp-for-opt-in-users", + "totp/embed", ], }, - "frontend-custom", { type: 'category', - label: 'Using pre-built UI', + label: 'Email / SMS OTP', + collapsed: true, items: [ - "pre-built-ui/init", - "pre-built-ui/showing-login-ui", - "pre-built-ui/protecting-routes" + "email-sms-otp/otp-for-all-users", + "email-sms-otp/otp-for-opt-in-users", + "email-sms-otp/embed" + ], + }, + "protect-routes", + "with-email-verification", + "step-up-auth", + "backup-codes", + { + type: 'category', + label: 'Migration', + collapsed: true, + items: [ + "migration", + "legacy-to-new", + "old-sdk-to-new" + ], + }, + "security", + { + type: 'category', + label: 'Legacy MFA method', + collapsed: true, + items: [ + "legacy-method/legacy-vs-new", + "legacy-method/how-it-works", + { + type: 'category', + label: 'Backend Setup', + collapsed: false, + items: [ + "legacy-method/backend/first-factor", + "legacy-method/backend/second-factor", + "legacy-method/backend/protecting-api", + ], + }, + "legacy-method/frontend-custom", + { + type: 'category', + label: 'Using pre-built UI', + items: [ + "legacy-method/pre-built-ui/init", + "legacy-method/pre-built-ui/showing-login-ui", + "legacy-method/pre-built-ui/protecting-routes" + ], + }, ], }, ] diff --git a/v2/mfa/step-up-auth.mdx b/v2/mfa/step-up-auth.mdx new file mode 100644 index 000000000..cdc35d267 --- /dev/null +++ b/v2/mfa/step-up-auth.mdx @@ -0,0 +1,870 @@ +--- +id: step-up-auth +title: Step up auth +hide_title: true +show_ui_switcher: true +--- + +import MFAPaidBanner from '../community/reusableMD/mfa/MFAPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; +import TabItem from '@theme/TabItem'; +import PythonSyncAsyncSubTabs from "/src/components/tabs/PythonSyncAsyncSubTabs"; +import CoreInjector from "/src/components/coreInjector" +import CustomAdmonition from "/src/components/customAdmonition" +import NodeJSFrameworkSubTabs from "/src/components/tabs/NodeJSFrameworkSubTabs"; +import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" +import FrontendPreBuiltUITabs from "/src/components/tabs/FrontendPreBuiltUITabs" +import FrontendCustomUITabs from "/src/components/tabs/FrontendCustomUITabs" +import NpmOrScriptTabs from "/src/components/tabs/NpmOrScriptTabs" +import FrontendMobileSubTabs from "/src/components/tabs/FrontendMobileSubTabs" + + + +# Step up auth + +Step up auth is when you want the user to complete an auth challenge before navigating to a page, or before doing an action on a page. + +SuperTokens allows you to implement step up auth using the following factors: +- TOTP +- Password (available only for custom UI) +- Email / SMS OTP + +You can implement these as full page navigations, or as popups on the current page. + +## Step 1) Adding backend validators +To protect sensitive APIs with step up auth, you need to check that the user has completed the required auth challenge within a certain amount of time. If they haven't, you should return a `403` to the frontend which highlights which factor is required. The frontend can then consume this and show the auth challenge to the user. + + + + + + +```tsx +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import express from "express"; +import { SessionRequest } from "supertokens-node/framework/express"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +let app = express(); + +app.post( + "/update-blog", + verifySession(), + async (req: SessionRequest, res) => { + let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + // continue with API logic... + } +); +``` + + + +```tsx +import Hapi from "@hapi/hapi"; +import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; +import {SessionRequest} from "supertokens-node/framework/hapi"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +let server = Hapi.server({ port: 8000 }); + +server.route({ + path: "/update-blog", + method: "post", + options: { + pre: [ + { + method: verifySession(), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + // continue with API logic... + + } +}) +``` + + + +```tsx +import Fastify from "fastify"; +import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; +import { SessionRequest } from "supertokens-node/framework/fastify"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +let fastify = Fastify(); + +fastify.post("/update-blog", { + preHandler: verifySession(), +}, async (req: SessionRequest, res) => { + let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + // continue with API logic... +}); +``` + + + + +```tsx +import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; +import { SessionEvent } from "supertokens-node/framework/awsLambda"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +async function updateBlog(awsEvent: SessionEvent) { + let mfaClaim = await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + // continue with API logic... +}; + +exports.handler = verifySession(updateBlog); +``` + + + + +```tsx +import KoaRouter from "koa-router"; +import { verifySession } from "supertokens-node/recipe/session/framework/koa"; +import { SessionContext } from "supertokens-node/framework/koa"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +let router = new KoaRouter(); + +router.post("/update-blog", verifySession(), async (ctx: SessionContext, next) => { + let mfaClaim = await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + // continue with API logic... +}); +``` + + + + +```tsx +import { inject, intercept } from "@loopback/core"; +import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; +import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; +import Session from "supertokens-node/recipe/session"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +class Example { + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } + @post("/update-blog") + @intercept(verifySession()) + @response(200) + async handler() { + let mfaClaim = await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + // continue with API logic... + } +} +``` + + + + +```tsx +import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import { SessionRequest } from "supertokens-node/framework/express"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +// highlight-start +export default async function example(req: SessionRequest, res: any) { + await superTokensNextWrapper( + async (next) => { + await verifySession()(req, res, next); + }, + req, + res + ) + let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + await superTokensNextWrapper( + async (next) => { + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + }, + req, + res + ) + } + // continue with API logic... +} +``` + + + + +```tsx +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +// @ts-ignore +import { backendConfig } from "@/app/config/backend"; +import { Error as STError } from "supertokens-node/recipe/session" + +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + let mfaClaim = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + const error = new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + return NextResponse.json(error, { status: 403 }); + } + // continue with API logic... + return NextResponse.json({}) + }); +} +``` + + + + +```tsx +import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; +import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; +// @ts-ignore +import { AuthGuard } from './auth/auth.guard'; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session" + +@Controller() +export class ExampleController { + @Post('example') + @UseGuards(new AuthGuard()) + async postExample(@Session() session: SessionContainer): Promise { + let mfaClaim = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + // continue with API logic... + return true; + } +} +``` + + + + + + +:::note +Coming soon. +::: + + + + +:::note +Coming soon. +::: + + + + +- When calling the `verifySession`, SuperTokens makes sure that the session is valid and that the user has completed all the requreied auth factors at some point in time. This enforces the basic check that the user has finished MFA during login. +- We then further check that if the user has finished the TOTP login method within the last 5 mins. If they haven't, we send back a 403 to the frontend for the frontend to handle. +- You can check other factor types in this was as well. For example, if you want to check that the user has done email OTP in the last 5 mins, you can use the factor ID of `otp-email`, or if you want to check that the user has entered their account password in the last 5 mins, you can check `emailpassword` factor ID. +- If users have different login methods, and / or different MFA configurations, you may want to first check what factor applies to them. You can check their login method by fetching the user object using the `getUser` function from our SDK, and then matching the `session.getRecipeId()` to the login methods in the user object. As per the MFA factors, you can see which ones are enabled for this user by using the `MultiFactorAuth.getRequiredSecondaryFactorsForUser` function. For performance reasons, you may want to put this information in the session's access token payload of the user in the `createNewSession` override function of the session recipe. + +## Step 2) Preventing factor setup during step up auth +By default, SuperTokens allows a factor setup (for example, creating a new TOTP device), as long as the user has a session and has completed all the MFA factors required during login. This opens up a security issue when it comes to completing step up auth. Consider the following scenario: +- The user has logged in and completed TOTP +- After 5 mins, the user tries to do a sensitive action and the API for that fails with a 403 (cause of the check in step 1, above). +- The user is shown the TOTP challenge on the frontend. However, instead of completing that, they call the create TOTP device API which would succeed and then use the new TOTP device to complete the factor challenge required for the API. + +This allows someome malicious to bypass step up auth. In order to prevent this, we need to override one fo the MFA recipe functions on the backend to enforce that the factor setup can only happen if the user is not in a step up auth state: + + + + +```ts +import supertokens from "supertokens-node"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import { Error as STError } from "supertokens-node/recipe/session" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + + MultiFactorAuth.init({ + firstFactors: [/*...*/], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // highlight-start + assertAllowedToSetupFactorElseThrowInvalidClaimError: async (input) => { + await originalImplementation.assertAllowedToSetupFactorElseThrowInvalidClaimError(input); + + let claimValue = await input.session.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (claimValue === undefined || !claimValue.v) { + return + } + + // if the above did not throw, it means that the user has logged in and has completed all the required + // factors for login. So now we check specifically for the step up auth case: + if (input.factorId === "totp" && (await input.factorsSetUpForUser).includes("totp")) { + // this is an example of checking for totp, but you can also use other factor IDs. + const totpCompletedTime = claimValue.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000 * 60 * 5)) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [{ + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }] + }) + } + } + } + // highlight-end + } + } + } + }) + ] +}) +``` + + + + +:::note +Coming soon. +::: + + + + +:::note +Coming soon. +::: + + + + +- The function `assertAllowedToSetupFactorElseThrowInvalidClaimError` is called by SuperTokens whenever the client calls an API to setup a new factor (for example, create a new TOTP device). So we do our checks in this function and throw an error in case we have to to prevent factor setup. +- In the override logic, we first call the original implementation and check that the `v` value in the MFA session claim is `true`. This will throw / exit the function early in case the user has not completely logged in yet (for example, they have finished the first factor, but not the required second factor). +- Then we check if the user has TOTP already setup for them, if they haven't, then we allow the factor setup (otherwise the user would not be able to complete the step up auth challenge). If they have, we do the same check we did in step 1 - checking if the user has finished TOTP in the last 5 mins or not. If they haven't, we disallow factor setup. + +The customisation above prevents the security issue highlighted in the beginning of this step. + +## Step 3) Handling `403` on the frontend +The JSON body of the step up auth claim failure will look like this: + +```json +{ + "message": "invalid claim", + "claimValidationErrors": [ + { + "id": "st-mfa", + "reason": { + "message": "Factor validation failed: totp not completed", + "factorId": "totp", + } + } + ] +} +``` + + + + + +You can check for this structure and the `factorId` to decide what factor to show on the frontend. You have two options to show the UI to the user: + +### Full page redirect to the factor +To redirect the user to as factor challenge page and then navigate them back to the current page, you can use the following function: + + + + +```tsx +import MultiFactorAuth from 'supertokens-auth-react/recipe/multifactorauth'; + +async function redirectToTotpSetupScreen() { + MultiFactorAuth.redirectToFactor("totp", false, true) +} +``` + +- In the snippet above, we redirect to the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option). The second argument represents a boolean for `forceSetup` which we set to false since we do not want to ask the user to create a new device. The third arg is also `true` since we want to redirect back to the current page after the user has finished setting up the device. +- You can also just redirect the user to `/{websiteBasePath}/mfa/totp?redirectToPath={currentPath}` if you don't want to use the above function. + + + + + + +In order to add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?redirectToPath={currentPath}` from your settings page. This will show the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) to the user. The `redirectToPath` query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device. + + + + + +### Show the factor in a popup +Checkout the docs for embedding the pre built UI factor components in a page / popup: +- [TOTP](./totp/embed) +- [Email / SMS OTP](./email-sms-otp/embed) + + + + + +You can check for this structure and the `factorId` to decide what factor to show on the frontend. + + + + + +## Step 4) Checking for step up auth on page navigation + +Sometimes, you may want to ask users to complete step up auth before displaying a page on the frontend. This is a different scenario that the above steps cause here, you do not want to reply on an API call to fail, instead you want to check for the step up auth condition before rendering the page itself. + +To do this, we read the access token payload on the frontend and check the completed time of the factor we care about before rendering the page. If the completed time is older than 5 mins (as an example), we should redirect the user to the factor challenge page. + + + + + + + + + +```tsx +import React from "react"; +import { SessionAuth, useClaimValue } from 'supertokens-auth-react/recipe/session'; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +import { DateProviderReference } from "supertokens-auth-react/utils/dateProvider" + +const VerifiedRoute = (props: React.PropsWithChildren) => { + return ( + + + {props.children} + + + ); +} + +function InvalidClaimHandler(props: React.PropsWithChildren) { + let claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (claimValue.loading) { + return null; + } + + let totpCompletedTime = claimValue.value?.c["totp"] + if (totpCompletedTime === undefined || totpCompletedTime < (DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { + return
      You need to complete TOTP before seeing this page. Please click here to finish to proceed.
      + } + + // the user has finished TOTP, so we can render the children + return
      {props.children}
      ; +} +``` +- We check if the user has completed TOTP within the last 5 mins or not. If not, we show a message to the user, and ask them to complete TOTP. +- Notice that we use a `DateProviderReference` class exported by SuperTokens instead of just doing `Date.now()`. This is done to take into account any clock skew that may exist between the frontend and the backend server. + +
      + + + +```tsx +import Session from "supertokens-web-js/recipe/session"; +import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; +import { DateProviderReference } from "supertokens-web-js/utils/dateProvider"; + +async function shouldLoadRoute(): Promise { + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims(); + + if (validationErrors.length === 0) { + // since all default claim validators have passed, we now check for if the user has finished TOTP + // within the last 5 mins + let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); + let totpCompletedTime = mfaClaimValue?.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { + // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. + return false; + } + return true; + } else { + // handle other validation failure events... + } + } + // a session does not exist, or email is not verified + return false +} +``` + +- In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. If that passes, it means all the default claim validators have passed (checks that are applied to all routes in general), and we can check for the step up auth check next. +- For checking for step up auth, we get the MFA claim value from the session and then check if TOTP was completed within the last 5 mins. Only if it was, we return true, else we return false. +- Notice that we use a `DateProviderReference` class exported by SuperTokens instead of just doing `Date.now()`. This is done to take into account any clock skew that may exist between the frontend and the backend server. + + + + + +~COPY-TABS=angular + + + +
      + +
      + + + + + + + + + + +```tsx +import Session from "supertokens-web-js/recipe/session"; +import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; +import { DateProviderReference } from "supertokens-web-js/utils/dateProvider"; + +async function shouldLoadRoute(): Promise { + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims(); + + if (validationErrors.length === 0) { + // since all default claim validators have passed, we now check for if the user has finished TOTP + // within the last 5 mins + let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); + let totpCompletedTime = mfaClaimValue?.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { + // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. + return false; + } + return true; + } else { + // handle other validation failure events... + } + } + // a session does not exist, or email is not verified + return false +} +``` + + + + +```tsx +import supertokensSession from "supertokens-web-js-script/recipe/session"; +import supertokensMultiFactorAuth from "supertokens-web-js-script/recipe/multifactorauth"; +import supertokensDateProviderReference from "supertokens-web-js-script/utils/dateProvider"; +async function shouldLoadRoute(): Promise { + if (await supertokensSession.doesSessionExist()) { + let validationErrors = await supertokensSession.validateClaims(); + + if (validationErrors.length === 0) { + // since all default claim validators have passed, we now check for if the user has finished TOTP + // within the last 5 mins + let mfaClaimValue = await supertokensSession.getClaimValue({ claim: supertokensMultiFactorAuth.MultiFactorAuthClaim }); + let totpCompletedTime = mfaClaimValue?.c["totp"]; + if (totpCompletedTime === undefined || totpCompletedTime < (supertokensDateProviderReference.DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { + // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. + return false; + } + return true; + } else { + // handle other validation failure events... + } + } + // a session does not exist, or email is not verified + return false +} +``` + + + + +- In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. If that passes, it means all the default claim validators have passed (checks that are applied to all routes in general), and we can check for the step up auth check next. +- For checking for step up auth, we get the MFA claim value from the session and then check if TOTP was completed within the last 5 mins. Only if it was, we return true, else we return false. +- Notice that we use a `DateProviderReference` class exported by SuperTokens instead of just doing `Date.now()`. This is done to take into account any clock skew that may exist between the frontend and the backend server. + + + + + + + + + +```tsx +import SuperTokens from 'supertokens-react-native'; + +async function checkIfMFAIsCompleted() { + if (await SuperTokens.doesSessionExist()) { + + // highlight-start + let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; + if (isMFACompleted) { + let completedFactors = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].c; + if (completedFactors["totp"] === undefined || completedFactors["totp"] < (Date.now() - 1000*60*5)) { + // user has not finished TOTP MFA in the last 5 minutes + } + } else { + // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user + } + // highlight-end + } +} +``` + + + + + +```kotlin +import android.app.Application +import com.supertokens.session.SuperTokens +import org.json.JSONObject + +class MainApplication: Application() { + fun checkIfMFAIsCompleted() { + val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this); + val isMFACompleted: Boolean = (accessTokenPayload.get("st-mfa") as JSONObject).get("v") as Boolean + if (isMFACompleted) { + // All required factors for MFA have been completed + val completedFactors: Boolean = (accessTokenPayload.get("st-mfa") as JSONObject).get("c") as Map; + if (!completedFactors.contains("totp") || completedFactors.get("totp") < System.currentTimeInMill() - 1000*60*5) { + // user has not finished TOTP MFA in the last 5 minutes + } + } else { + // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user + } + } +} +``` + + + + + +```swift +import UIKit +import SuperTokensIOS + +fileprivate class ViewController: UIViewController { + func checkIfMFAIsCompleted() { + if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely(), let mfaObject: [String: Any] = mfaObject["st-mfa"] as? [String: Any], let isMFACompleted: Bool = mfaObject["v"] as? Bool { + if isMFACompleted { + // All required factors for MFA have been completed + let mfaCompletedFactors: [String: Any] = mfaObject["c"] as? [String: Any]; + if mfaCompletedFactors["totp"] == nil || mfaCompletedFactors["totp"] < (Date().timeIntervalSince1970 - 1000*60*5) { + // user has not finished TOTP MFA in the last 5 minutes + } + } else { + // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user + } + } + } +} +``` + + + + + +```dart +import 'package:supertokens_flutter/supertokens.dart'; + +Future checkIfMFAIsCompleted() async { + var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); + + if (accessTokenPayload.containsKey("st-mfa")) { + Map mfaObject = accessTokenPayload["st-mfa"]; + + if (mfaObject.containsKey("v")) { + bool isMFACompleted = mfaObject["v"]; + + if (isMFACompleted) { + // All required factors for MFA have been completed + Map mfaCompletedFactors = mfaObject["c"]; + if (mfaCompletedFactors["totp"] == null || mfaCompletedFactors["totp"] < (DateTime.now().millisecondsSinceEpoch - 1000*60*5)) { + // user has not finished TOTP MFA in the last 5 minutes + } + } else { + // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user + } + } + } +} +``` + + + + + +- In your protected routes, you need to first check if a session exists, and then check that the user has finished all the basic MFA factors for logging in (by checking the value of the `v` boolean in the MFA claim session). If that passes, we can check for the step up auth check next. +- For checking for step up auth, we get the MFA claim value from the session, and then check if TOTP was completed within the last 5 mins. Only if it was, we return true, else we return false. + + + + + + +
      diff --git a/v2/mfa/totp/embed.mdx b/v2/mfa/totp/embed.mdx new file mode 100644 index 000000000..ec2904d94 --- /dev/null +++ b/v2/mfa/totp/embed.mdx @@ -0,0 +1,423 @@ +--- +id: embed +title: Embed our pre built UI component +hide_title: true +show_ui_switcher: true +--- + +import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" +import FrontendPreBuiltUITabs from "/src/components/tabs/FrontendPreBuiltUITabs" +import TabItem from '@theme/TabItem'; +import {Question, Answer}from "/src/components/question" +import RRDVersionSubTabs from "/src/components/tabs/RRDVersionSubTabs" + +# Embed our pre built UI component + + + + + +## Case 1: Rendering the TOTP Widget in a page + +The following example shows the scenario where you have a dedicated route, such as `/totp`, for rendering the TOTP Widget. Upon a successful login, the user will be automatically redirected to the return value of `getRedirectionURL` (defaulting to `/`). + + + + + + + + + + +```tsx +import SuperTokens from "supertokens-auth-react"; +import TOTP from "supertokens-auth-react/recipe/totp"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +// highlight-start +import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; +// highlight-end +// @ts-ignore +import Header from "./header"; +// @ts-ignore +import Footer from "./footer"; +import { useNavigate } from "react-router-dom"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + TOTP.init({ + totpMFAScreen: { + disableDefaultUI: true, + } + }), + MultiFactorAuth.init({ + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "GO_TO_FACTOR") { + if (context.factorId === "totp") { + return "/totp" + } + } + } + // highlight-end + }) + // ... + ], +}); + +function TOTPPage() { + const navigate = useNavigate(); + return ( +
      +
      + // highlight-next-line + +
      +
      + ); +} +``` + +
      + + + +```tsx +import React from "react"; +import SuperTokens from "supertokens-auth-react"; +import TOTP from "supertokens-auth-react/recipe/totp"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +// highlight-start +import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; +// highlight-end +// @ts-ignore +import Header from "./header"; +// @ts-ignore +import Footer from "./footer"; +import { useHistory } from "react-router-dom5"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + TOTP.init({ + totpMFAScreen: { + disableDefaultUI: true, + }, + }), + MultiFactorAuth.init({ + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "GO_TO_FACTOR") { + if (context.factorId === "totp") { + return "/totp" + } + } + } + // highlight-end + }) + // ... + ], +}); + +function TOTPPage() { + const history = useHistory(); + return ( +
      +
      + // highlight-next-line + +
      +
      + ); +} +``` + +
      + +
      + +
      + + +```tsx +import React from "react"; +import SuperTokens from "supertokens-auth-react"; +import TOTP from "supertokens-auth-react/recipe/totp"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +// highlight-start +import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; +// highlight-end +// @ts-ignore +import Header from "./header"; +// @ts-ignore +import Footer from "./footer"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + TOTP.init({ + totpMFAScreen: { + disableDefaultUI: true, + }, + }), + MultiFactorAuth.init({ + getRedirectionURL: async (context) => { + if (context.action === "GO_TO_FACTOR") { + if (context.factorId === "totp") { + return "/totp" + } + } + } + }) + // ... + ], +}); + +function TOTPPage() { + return ( +
      +
      + // highlight-next-line + +
      +
      + ) +} +``` +
      +
      +
      +
      + +In the above code snippet, we: + +1. Disabled the default TOTP UI by setting `disableDefaultUI` to `true` inside the TOTP recipe config. +2. Overrode the `getRedirectionURL` function inside the MFA recipe config to redirect to `/totp` whenever we want to show the TOTP factor. + +Feel free to customize the redirection URLs as needed. + +## Case 2: Rendering the TOTP Widget in a popup + +The following example shows the scenario where you embed the TOTP Widget in a popup, and upon successful login, you aim to close the popup. This is especially useful for step up auth. + + + + + + + + + + +```tsx +import React, { useState, useEffect } from "react"; +import Modal from "react-modal"; +import SuperTokens from "supertokens-auth-react"; +import TOTP from "supertokens-auth-react/recipe/totp"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +// highlight-start +import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; +import Session from "supertokens-auth-react/recipe/session"; +// highlight-end +import { useNavigate } from "react-router-dom"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + TOTP.init(/* ... */), + MultiFactorAuth.init(/* ... */) + // ... + ], +}); + +function TOTPPopup() { + let sessionContext = Session.useSessionContext(); + const navigate = useNavigate(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const openModal = () => setIsModalOpen(true); + const closeModal = () => setIsModalOpen(false); + + if (sessionContext.loading) { + return null; + } + + return ( +
      + { + +

      You are logged In!

      +

      UserId: {sessionContext.userId}

      + +
      + } + + + {/* highlight-next-line */} + + +
      + ); +} +``` + +
      + + + +```tsx +import React, { useState, useEffect } from "react"; +import Modal from "react-modal"; +import SuperTokens from "supertokens-auth-react"; +import TOTP from "supertokens-auth-react/recipe/totp"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +// highlight-start +import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; +import Session from "supertokens-auth-react/recipe/session"; +// highlight-end +import { useHistory } from "react-router-dom5"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + TOTP.init(/* ... */), + MultiFactorAuth.init(/* ... */) + // ... + ], +}); + +function TOTPPopup() { + let sessionContext = Session.useSessionContext(); + const history = useHistory(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const openModal = () => setIsModalOpen(true); + const closeModal = () => setIsModalOpen(false); + + if (sessionContext.loading) { + return null; + } + + return ( +
      + { + +

      You are logged In!

      +

      UserId: {sessionContext.userId}

      + +
      + } + + + {/* highlight-next-line */} + + +
      + ); +} +``` + +
      + +
      + +
      + + +```tsx +import React, { useState, useEffect } from "react"; +import Modal from "react-modal"; +import SuperTokens from "supertokens-auth-react"; +import TOTP from "supertokens-auth-react/recipe/totp"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +// highlight-start +import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; +import Session from "supertokens-auth-react/recipe/session"; +// highlight-end + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + TOTP.init(/* ... */), + MultiFactorAuth.init(/* ... */) + // ... + ], +}); + +function TOTPPopup() { + let sessionContext = Session.useSessionContext(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const openModal = () => setIsModalOpen(true); + const closeModal = () => setIsModalOpen(false); + + if (sessionContext.loading) { + return null; + } + + return ( +
      + { + +

      You are logged In!

      +

      UserId: {sessionContext.userId}

      + +
      + } + + + {/* highlight-next-line */} + + +
      + ); +} +``` +
      +
      +
      +
      + +
      + + +This guide is not applicable for Custom UI. + + +
      \ No newline at end of file diff --git a/v2/mfa/totp/totp-for-all-users.mdx b/v2/mfa/totp/totp-for-all-users.mdx new file mode 100644 index 000000000..a67eb6e3f --- /dev/null +++ b/v2/mfa/totp/totp-for-all-users.mdx @@ -0,0 +1,1051 @@ +--- +id: totp-for-all-users +title: TOTP required for all users +hide_title: true +show_ui_switcher: true +--- + +import MFAPaidBanner from '../../community/reusableMD/mfa/MFAPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; +import TabItem from '@theme/TabItem'; +import PythonSyncAsyncSubTabs from "/src/components/tabs/PythonSyncAsyncSubTabs"; +import CoreInjector from "/src/components/coreInjector" +import CustomAdmonition from "/src/components/customAdmonition" +import {Question, Answer}from "/src/components/question" +import FrontendPreBuiltUITabs from "/src/components/tabs/FrontendPreBuiltUITabs" +import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" +import FrontendCustomUITabs from "/src/components/tabs/FrontendCustomUITabs" +import NpmOrScriptTabs from "/src/components/tabs/NpmOrScriptTabs" +import AppInfoForm from "/src/components/appInfoForm" +import FrontendMobileSubTabs from "/src/components/tabs/FrontendMobileSubTabs" + + + +# TOTP required for all users + +In this page, we will show you how to implement an MFA policy that requires all users to use TOTP before they get access to your application. + +:::note +We assume that the first factor is [email password or social login](/docs/thirdpartyemailpassword/introduction), but the same set of steps will be applicable for other first factor types as well. +::: + +## Single tenant setup + +### Backend setup +To start with, we configure the backend in the following way: + + + + +```ts +import supertokens from "supertokens-node"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import totp from "supertokens-node/recipe/totp" +import Session from "supertokens-node/recipe/session" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + ThirdPartyEmailPassword.init({ + //... + }), + // highlight-next-line + totp.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + return ["totp"] + } + } + } + } + // highlight-end + }) + ] +}) +``` + + + + +:::note +Coming soon. +::: + + + + +:::note +Coming soon. +::: + + + + +- Notice that we have initialised the TOTP recipe in the `recipeList`. By default, no configs are required for it, but you can provide: + - `issuer`: This is the name that will show up in the TOTP app for the user. By default, this is equal to the `appName` config, however, you can change it to something else using this property. + - `defaultSkew`: The default value of this is `1`, which means that TOTP codes that were generated 1 tick before, and that will be generated 1 tick after from the current tick will be accepted at any given time (including the TOTP of the current tick, of course). + - `defaultPeriod`: The default value of this is `30`, which means that the current tick is valie for 30 seconds. So by default, a TOTP code that's just shown to the user, is valid for 60 seconds (`defaultPeriod + defaultSkew*defaultPeriod` seconds) +- We also override the `getMFARequirementsForAuth` function to indicate that `totp` must be completed before the user can access the app. Notice that we do not check for the userId there, and return `totp` for all users. + +Once the user finishes the first factor (for example, with emailpassword), their session access token payload will look like this: +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + }, + "v": false + } +} +``` + +The `v` being `false` indicates that there are still factors that are pending. After the user has finished totp, the payload will look like: + +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "totp": 1702877999 + }, + "v": true + } +} +``` + +Indicating that the user has finished all required factors, and should be allowed to access the app. + +### Frontend setup + + + + + +We start by modifying the `init` function call on the frontend like so: + + + + + +```tsx +import supertokens from "supertokens-auth-react" +import ThirdPartyEmailPassword from "supertokens-auth-react/recipe/thirdpartyemailpassword" +import Passwordless from "supertokens-auth-react/recipe/passwordless" +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" +import totp from "supertokens-auth-react/recipe/totp" + +supertokens.init({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdPartyEmailPassword.init( /* ... */), + // highlight-start + totp.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"] + }) + // highlight-end + ] +}) +``` + + + + + +- Just like on the backend, we init the `totp` recipe in the `recipeList`. +- We also init the `MultiFactorAuth` recipe, and pass in the first factors that we want to use. In this case, that would be `emailpassword` and `thirdparty` - same as the backend. + +Next, we need to add the TOTP pre built UI when rendering the SuperTokens component: + + + + + + + + +```tsx +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import { ThirdPartyEmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui"; +import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui"; +import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; + +function App() { + return ( + +
      + +
      + + // highlight-start + {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ThirdPartyEmailPasswordPreBuiltUI, TOTPPreBuiltUI])} + // highlight-end + // ... other routes + +
      +
      +
      +
      + ); +} +``` + +
      + + + +```tsx +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; +import { ThirdPartyEmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui"; +import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui"; + +function App() { + // highlight-start + if (canHandleRoute([ThirdPartyEmailPasswordPreBuiltUI, TOTPPreBuiltUI])) { + return getRoutingComponent([ThirdPartyEmailPasswordPreBuiltUI, TOTPPreBuiltUI]) + } + // highlight-end + return ( + {/*Your app*/} + ); +} +``` + + + +
      + +
      + +
      + +With the above configuration, users will see emailpassword or social login UI when they visit the auth page. After completing that, users will be redirected to `/auth/mfa/totp` (assuming that the `websiteBasePath` is `/auth`) where they will be asked to setup the factor, or complete the TOTP challenge if they have already setup the factor before. The UI for this screen looks like: +- [Factor Setup UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) +- [Verification UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--verification-with-single-next-option) (In case the factor is already setup before). + +
      + + + +We start by initialising the MFA and TOTP recipe on the frontend like so: + + + + + + + + + +```tsx +import SuperTokens from 'supertokens-web-js'; +import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; +import Totp from "supertokens-web-js/recipe/totp"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + // highlight-start + MultiFactorAuth.init(), + Totp.init() + // highlight-end + ], +}); +``` + + + + + +```tsx +import supertokens from "supertokens-web-js-script"; +import supertokensMultiFactorAuth from 'supertokens-web-js-script/recipe/multifactorauth'; +import supertokensTotp from "supertokens-web-js-script/recipe/totp"; +supertokens.init({ + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + // highlight-start + supertokensMultiFactorAuth.init(), + supertokensTotp.init() + // highlight-end + ], +}); +``` + + + + + + + + + + +:::success +This step is not applicable for mobile apps. Please continue reading. +::: + + + + + +After the first factor login, you should start by [checking the access token payload and see if the MFA claim's `v` boolean is `false`](../frontend-setup#step-2-checking-the---custv-boolean-value-in-the-mfa-claim--cust). If it's not, then we can redeirect the user to the application page. + +If it's `false`, the frontend then needs to [call the MFA endpoint](../frontend-setup#mfa-info-endpoint) to get information about which factor the user should be asked to complete next. Based on the backend config in this page, the `next` array will contain `["totp"]`. + +There are two possibilities here: +- Case 1: The user needs to setup a TOTP device cause they don't have any. +- Case 2: The user already has a verified device setup and needs to complete the TOTP challenge. + +We can know which case it is by checking if `"totp"` is one of the items in the `factorsThatAreAlreadySetup` array that is returned from the API call above. If it is in the array, then it's case 2, otherwise it's case 1. + +#### Case 1 implementation: User needs to setup a new TOTP device +In this case, we do two things: +- Call an API on the backend to create a device. This returns the device secret that can be displayed to the user. The user is supposed to scan this using their authenticator app, to add a new entry for your app in their authenticator app. +- Then the user needs to enter the TOTP code that's displayed to them in the app, and this needs to be sent to the backend to mark the device as verified. Once a device is marked as verified, only then will the `factorsThatAreAlreadySetup` array contain `"totp"` the next time they login. + +To create a new device, call the following API: + + + + + + + + + +```tsx +import Totp from "supertokens-web-js/recipe/totp" +import Session from "supertokens-web-js/recipe/session" + +async function createNewTotpDevice() { + if (await Session.doesSessionExist()) { + try { + let deviceResponse = await Totp.createDevice(); + if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") { + // this should only come here if you are passing a custom device name when calling the above function. + throw new Error("Should never come here") + // device created successfully + } + // device created successfully + let qrCodeString = deviceResponse.qrCodeString; + let secret = deviceResponse.secret; + + // TODO: display a QR code based on qrCodeString, and also an option to view + // the secret if the user is unable to scan the QR code. + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("TOTP device creation can only happen after the first factor is complete and when a session exists") + } +} +``` + + + + + +```tsx +import supertokensTotp from "supertokens-web-js-script/recipe/totp" +import supertokensSession from "supertokens-web-js-script/recipe/session" +async function createNewTotpDevice() { + if (await supertokensSession.doesSessionExist()) { + try { + let deviceResponse = await supertokensTotp.createDevice(); + if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") { + // this should only come here if you are passing a custom device name when calling the above function. + throw new Error("Should never come here") + // device created successfully + } + // device created successfully + let qrCodeString = deviceResponse.qrCodeString; + let secret = deviceResponse.secret; + + // TODO: display a QR code based on qrCodeString, and also an option to view + // the secret if the user is unable to scan the QR code. + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("TOTP device creation can only happen after the first factor is complete and when a session exists") + } +} +``` + + + + + + + + + + + +Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): + +```bash +curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/totp/device' \ +--header 'rid: totp' \ +--header 'Authorization: Bearer ...' +``` + + + +The above API call returns the following response: + +```json +{ + "status": "OK", + "issuerName": "...", + "deviceName": "TOTP Device 1", + "secret": "....", + "userIdentifier": "user@example.com", + "qrCodeString": "..." +} | { + "status": "DEVICE_ALREADY_EXISTS_ERROR" | "GENERAL_ERROR" +} +``` + + + + + +- When device registeration is successful, the API returns: + - The `secret` and `qrCodeString` which are to be displayed to the user. For React apps, we recommend using the [react-qr-code library](https://github.com/rosskhanas/react-qr-code) to display the QR code. + - The `issuerName` is the name will show up on the TOTP app for the user. By default, this is equal to the `appName` config on the backend sdk, however, you can change it to something else in the backend `totp.init` config. + - The `userIdentifier` is the email / phone number of the user based on the first factor. This will also be shown in the TOTP app along with the `issuerName`. +- The API call can also take a `deviceName` (as a POST body prop) which attemps to create a TOTP device with the provideed name. A status of `"DEVICE_ALREADY_EXISTS_ERROR"` is returned in case a verified device with the input name already exists. In this case, you should ask the user to enter a different name. Note that this status is only returned in case you are passing in a custom device name. The default naming strategy is to name the device "TOTP Device N", where we start N from 1, and keep increasing it. This value can be used to identify a device from the backend point of view, for operations like deleting a device. +- A status of `"GENERAL_ERROR"` is returned in case you specifically return that from a backend API override. + +Once a device has been created, and scanned, you need to ask the user to enter the TOTP and call the API below to verify it: + + + + + + + + + +```tsx +import Totp from "supertokens-web-js/recipe/totp" +import Session from "supertokens-web-js/recipe/session" + +async function verifyTotpDevice(deviceName: string, userInputTotp: string) { + if (await Session.doesSessionExist()) { + try { + let verifyResponse = await Totp.verifyDevice({ + deviceName, + totp: userInputTotp, + }); + if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") { + // this can happen due to a race condition wherein the device is deleted before verifying. + window.alert("Something went wrong. Please reload and try again"); + } else if (verifyResponse.status === "LIMIT_REACHED_ERROR") { + // this can happen if the user has entered a wrong TOTP too many times. + window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); + } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { + window.alert("Totp incorrect. Please try again"); + } else { + // Device verified successfully + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("TOTP device verification can only happen after the first factor is complete and when a session exists") + } +} +``` + + + + + +```tsx +import supertokensTotp from "supertokens-web-js-script/recipe/totp" +import supertokensSession from "supertokens-web-js-script/recipe/session" +async function verifyTotpDevice(deviceName: string, userInputTotp: string) { + if (await supertokensSession.doesSessionExist()) { + try { + let verifyResponse = await supertokensTotp.verifyDevice({ + deviceName, + totp: userInputTotp, + }); + if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") { + // this can happen due to a race condition wherein the device is deleted before verifying. + window.alert("Something went wrong. Please reload and try again"); + } else if (verifyResponse.status === "LIMIT_REACHED_ERROR") { + // this can happen if the user has entered a wrong TOTP too many times. + window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); + } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { + window.alert("Totp incorrect. Please try again"); + } else { + // Device verified successfully + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("TOTP device verification can only happen after the first factor is complete and when a session exists") + } +} +``` + + + + + + + + + + + +Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): + +```bash +curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/totp/device/verify' \ +--header 'rid: totp' \ +--header 'Authorization: Bearer ...' +--header 'Content-Type: application/json' \ +--data-raw '{ + "deviceName": "...", + "totp": "..." +}' +``` + + + +The above API call returns the following response: + +```json +{ + "status": "OK", + "wasAlreadyVerified": false +} | { + "status": "INVALID_TOTP_ERROR", + "currentNumberOfFailedAttempts": 1, + "maxNumberOfFailedAttempts": 5 +} | { + "status": "LIMIT_REACHED_ERROR", + "retryAfterMs": 900000 +} | { + "status": "UNKNOWN_DEVICE_ERROR" | "GENERAL_ERROR" +} +``` + + + + + +- The `deviceName`, which is an input to the API is one of the props returned from the previous API call to create a device. +- When verification is successful (`status: "OK"`), the device is marked as verified in the database and can be used for the TOTP challenge next time around. The boolean `wasAlreadyVerified` indicates if the device was already verified before this call was made. +- A status of `INVALID_TOTP_ERROR` means that the user has entered the an incorrect TOTP and needs to retry. The response contains two other props: + - `currentNumberOfFailedAttempts`: The number of times the user has entered an incorrect TOTP so far. + - `maxNumberOfFailedAttempts`: The maximum number of times the user can enter an incorrect TOTP before they are asked to wait (see `status: LIMIT_REACHED_ERROR`). This is set to 5 by default in the core. You can change this value by setting the `totp_max_attempts` in the core config. +- A status of `LIMIT_REACHED_ERROR` indicates that the user has entered an incorrect TOTP too many times and must wait before trying again (otherwise even valie TOTPs will fail). The waiting period is indicated by the `retryAfterMs` prop in the response body. By default, it is 15 mins, but it can be chanegd by setting the value for `totp_rate_limit_cooldown_sec` in the core config. +- A status of `UNKNOWN_DEVICE_ERROR` is possible due to a racce condition in which the device is somehow deleted before the verification call is made. +- A status of `GENERAL_ERROR` is possible if you specifically return that from a backend API override. + +On successful verification of a device, the `totp` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](../frontend-setup#mfa-info-endpoint). + +#### Case 2 implementation: User needs to complete the TOTP challenge + +This case is when the user already has a device setup (`totp` is in `factorsThatAreAlreadySetup`), and needs to complete the TOTP challenge. In this case, you should show the user an input box asking them to enter their TOTP from the authenticator app and then call the following API: + + + + + + + + + +```tsx +import Totp from "supertokens-web-js/recipe/totp" +import Session from "supertokens-web-js/recipe/session" + +async function verifyTotpCode(userInputTotp: string) { + if (await Session.doesSessionExist()) { + try { + let verifyResponse = await Totp.verifyCode({ + totp: userInputTotp, + }); + if (verifyResponse.status === "LIMIT_REACHED_ERROR") { + // this can happen if the user has entered a wrong TOTP too many times. + window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); + } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { + window.alert("Totp incorrect. Please try again"); + } else { + // Code verified successfully + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("TOTP code verification can only happen after the first factor is complete and when a session exists") + } +} +``` + + + + + +```tsx +import supertokensTotp from "supertokens-web-js-script/recipe/totp" +import supertokensSession from "supertokens-web-js-script/recipe/session" +async function verifyTotpCode(userInputTotp: string) { + if (await supertokensSession.doesSessionExist()) { + try { + let verifyResponse = await supertokensTotp.verifyCode({ + totp: userInputTotp, + }); + if (verifyResponse.status === "LIMIT_REACHED_ERROR") { + // this can happen if the user has entered a wrong TOTP too many times. + window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); + } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { + window.alert("Totp incorrect. Please try again"); + } else { + // Code verified successfully + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("TOTP code verification can only happen after the first factor is complete and when a session exists") + } +} +``` + + + + + + + + + + + +Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): + +```bash +curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/totp/verify' \ +--header 'rid: totp' \ +--header 'Authorization: Bearer ...' +--header 'Content-Type: application/json' \ +--data-raw '{ + "totp": "..." +}' +``` + + + +The above API call returns the following response: + +```json +{ + "status": "OK" | "UNKNOWN_USER_ID_ERROR" +} | { + "status": "INVALID_TOTP_ERROR", + "currentNumberOfFailedAttempts": 1, + "maxNumberOfFailedAttempts": 5, +} | { + "status": "LIMIT_REACHED_ERROR", + "retryAfterMs": 900000, +} | { + "status": "GENERAL_ERROR" +} +``` + + + + + +- A `status: OK` indicates that verification was successful. SuperTokens tries and verifies the input TOTP against all verified devices that belong to this user. +- A status of `INVALID_TOTP_ERROR` means that the user has entered the an incorrect TOTP and needs to retry. The response contains two other props: + - `currentNumberOfFailedAttempts`: The number of times the user has entered an incorrect TOTP so far. + - `maxNumberOfFailedAttempts`: The maximum number of times the user can enter an incorrect TOTP before they are asked to wait (see `status: LIMIT_REACHED_ERROR`). This is set to 5 by default in the core. You can change this value by setting the `totp_max_attempts` in the core config. +- A status of `LIMIT_REACHED_ERROR` indicates that the user has entered an incorrect TOTP too many times and must wait before trying again (otherwise even valie TOTPs will fail). The waiting period is indicated by the `retryAfterMs` prop in the response body. By default, it is 15 mins, but it can be chanegd by setting the value for `totp_rate_limit_cooldown_sec` in the core config. +- A status of `UNKNOWN_USER_ID_ERROR` is possible due to a racce condition in which all devices that the user had are deleted by the time this API is called. In this case, you can ask users to setup a new device. +- A status of `GENERAL_ERROR` is possible if you specifically return that from a backend API override. + +On successful verification of the code, the `totp` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](../frontend-setup#mfa-info-endpoint). + + + +
      + +## Multi tenant setup + +In a multi tenancy setup, you may want to enable TOTP for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#single-tenant-setup) section above, so in this section, we will focus on enabling TOTP for all users within specific tenants. + +### Backend setup + +To start, we will initialise the TOTP and the MultiFactorAuth recipes in the following way: + + + + +```ts +import supertokens from "supertokens-node"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import totp from "supertokens-node/recipe/totp" +import Session from "supertokens-node/recipe/session" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + ThirdPartyEmailPassword.init({ + //... + }), + // highlight-start + totp.init(), + MultiFactorAuth.init() + // highlight-end + ] +}) +``` + + + + +:::note +Coming soon. +::: + + + + +:::note +Coming soon. +::: + + + + +Unlike the single tenant setup, we do not provide any config to the `MultiFactorAuth` recipe cause all the necessary configuration will be done on a tenant level. + +To configure TOTP requirement for a tenant, we can call the following API: + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function createNewTenant() { + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + emailPasswordEnabled: true, + thirdPartyEnabled: true, + firstFactors: ["emailpassword", "thirdparty"], + requiredSecondaryFactors: ["totp"] + }); + + if (resp.createdNew) { + // Tenant created successfully + } else { + // Existing tenant's config was modified. + } +} +``` + + + + +:::note +Coming soon. +::: + + + + +:::note +Coming soon. +::: + + + + + + + +```bash +curl --location --request PUT '^{coreInjector_uri_without_quotes}/appid-/recipe/multitenancy/tenant' \ +--header 'api-key: ^{coreInjector_api_key_without_quotes}' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "tenantId": "customer1", + "emailPasswordEnabled": true, + "thirdPartyEnabled": true, + "firstFactors": ["emailpassword", "thirdparty"], + "requiredSecondaryFactors": ["totp"] +}' +``` + + + + + + + +- In the above, we set the `firstFactors` to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either emailpassword or thirdparty. We also configure that `emailPasswordEnabled` and `thirdPartyEnabled` are enabled for the tenant. +- We set the `requiredSecondaryFactors` to `["totp"]` to indicate that TOTP is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. + +Once the user finishes the first factor (for example, with emailpassword), their session access token payload will look like this: +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + }, + "v": false + } +} +``` + +The `v` being `false` indicates that there are still factors that are pending. After the user has finished totp, the payload will look like: + +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "totp": 1702877999 + }, + "v": true + } +} +``` + +Indicating that the user has finished all required factors, and should be allowed to access the app. + + +### Frontend setup + + + + + +We start by modifying the `init` function call on the frontend like so: + + + + + +```tsx +import supertokens from "supertokens-auth-react" +import ThirdPartyEmailPassword from "supertokens-auth-react/recipe/thirdpartyemailpassword" +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" +import totp from "supertokens-auth-react/recipe/totp" +import Multitenancy from "supertokens-auth-react/recipe/multitenancy" + +supertokens.init({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + // highlight-next-line + usesDynamicLoginMethods: true, + recipeList: [ + ThirdPartyEmailPassword.init( /* ... */), + // highlight-start + totp.init(), + MultiFactorAuth.init(), + Multitenancy.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getTenantId: async (context) => { + return "TODO" + } + } + } + } + }) + // highlight-end + ] +}) +``` + + + + + +- Just like on the backend, we init the `totp` recipe in the `recipeList`. +- We also init the `MultiFactorAuth` recipe. Notice that unlike the single tenant setup, we do not specify the `firstFactors` here. That information is fetched based on the tenantId you provide the SDK with. +- We have set `usesDynamicLoginMethods: true` so that the SDK knows to fetch the login methods dynamically based on the tenantId. +- Finally, we init the multi tenancy recipe and provide a method for getting the tenantId. + +Next, we need to add the TOTP pre built UI when rendering the SuperTokens component: + + + + + + + + +```tsx +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import { ThirdPartyEmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui"; +import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui"; +import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; + +function App() { + return ( + +
      + +
      + + // highlight-start + {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ThirdPartyEmailPasswordPreBuiltUI, TOTPPreBuiltUI])} + // highlight-end + // ... other routes + +
      +
      +
      +
      + ); +} +``` + +
      + + + +```tsx +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; +import { ThirdPartyEmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui"; +import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui"; + +function App() { + // highlight-start + if (canHandleRoute([ThirdPartyEmailPasswordPreBuiltUI, TOTPPreBuiltUI])) { + return getRoutingComponent([ThirdPartyEmailPasswordPreBuiltUI, TOTPPreBuiltUI]) + } + // highlight-end + return ( + {/*Your app*/} + ); +} +``` + + + +
      + +
      + +
      + +With the above configuration, users will see the first and second factor based on the tenant configuration. For the tenant we configured above, users will see email password or social login first. After completing that, users will be redirected to `/auth/mfa/totp` (assuming that the `websiteBasePath` is `/auth`) where they will be asked to setup the factor, or complete the TOTP challenge if they have already setup the factor before. The UI for this screen looks like: +- [Factor Setup UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) +- [Verification UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--verification-with-single-next-option) (In case the factor is already setup before). + +
      + + + +The steps here are the same as in [the single tenant setup above](#frontend-setup). + + + +
      + + + + + +## Protecting frontend and backend routes + +See the section on [protecting frontend and backend routes](../protect-routes). + +## Frontend events, pre and post API hooks + +TODO.. + + diff --git a/v2/mfa/totp/totp-for-opt-in-users.mdx b/v2/mfa/totp/totp-for-opt-in-users.mdx new file mode 100644 index 000000000..2139e9d0a --- /dev/null +++ b/v2/mfa/totp/totp-for-opt-in-users.mdx @@ -0,0 +1,778 @@ +--- +id: totp-for-opt-in-users +title: TOTP for specific users +hide_title: true +show_ui_switcher: true +--- + +import MFAPaidBanner from '../../community/reusableMD/mfa/MFAPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; +import TabItem from '@theme/TabItem'; +import PythonSyncAsyncSubTabs from "/src/components/tabs/PythonSyncAsyncSubTabs"; +import CoreInjector from "/src/components/coreInjector" +import CustomAdmonition from "/src/components/customAdmonition" +import {Question, Answer}from "/src/components/question" +import FrontendPreBuiltUITabs from "/src/components/tabs/FrontendPreBuiltUITabs" +import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" +import FrontendCustomUITabs from "/src/components/tabs/FrontendCustomUITabs" +import NpmOrScriptTabs from "/src/components/tabs/NpmOrScriptTabs" +import AppInfoForm from "/src/components/appInfoForm" +import FrontendMobileSubTabs from "/src/components/tabs/FrontendMobileSubTabs" + + + +# TOTP for specific users + +In this page, we will show you how to implement an MFA policy that requires certain users to do TOTP. You can decide which those users are based on any criteria. For example: +- Only users that have an `admin` role require to do TOTP; OR +- Only users that have enabled TOTP on their account require to do TOTP; OR +- Only users that have a paid account require to do TOTP. + +Whatever the criteria is, the steps to implementing this type of a flow is the same. + +:::note +We assume that the first factor is [email password or social login](/docs/thirdpartyemailpassword/introduction), but the same set of steps will be applicable for other first factor types as well. +::: + +## Single tenant setup + +### Backend setup + +
      Example 1: Only enable TOTP for users that have an `admin` role + +To start with, we configure the backend in the following way: + + + + +```ts +import supertokens from "supertokens-node"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import totp from "supertokens-node/recipe/totp" +import Session from "supertokens-node/recipe/session" +import UserRoles from "supertokens-node/recipe/userroles" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + UserRoles.init(), + ThirdPartyEmailPassword.init({ + //... + }), + // highlight-next-line + totp.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) + if (roles.roles.includes("admin")) { + // we only want totp for admins + return ["totp"] + } else { + // no MFA for non admin users. + return [] + } + } + } + } + } + // highlight-end + }) + ] +}) +``` + + + + +:::note +Coming soon. +::: + + + + +:::note +Coming soon. +::: + + + + + +We override the `getMFARequirementsForAuth` function to indicate that `totp` must be completed only for users that have the `admin` role. You can also have any other criteria here. + +
      + +
      Example 2: Ask for TOTP only for users that have enabled TOTP on their account + +To start with, we configure the backend in the following way: + + + + +```ts +import supertokens from "supertokens-node"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth" +import totp from "supertokens-node/recipe/totp" +import Session from "supertokens-node/recipe/session" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + ThirdPartyEmailPassword.init({ + //... + }), + // highlight-start + totp.init({ + override: { + apis: (oI) => { + return { + ...oI, + verifyDevicePOST: async function (input) { + let response = await oI.verifyDevicePOST!(input); + if (response.status === "OK") { + // device successfully verified. We save that this user has enabled TOTP in the user metadata. + // The multifactorauth recipe will pick this value up next time the user is trying to login, and + // ask them to enter the TOTP code. + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.TOTP); + } + return response; + } + } + } + } + }), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + }) + // highlight-end + ] +}) +``` + + + + +:::note +Coming soon. +::: + + + + +:::note +Coming soon. +::: + + + + +We simply initialise the multi factor auth recipe here without any override to `getMFARequirementsForAuth`. The default implementation of this function already checks what factors are enabled for a user and returns those. So all we need to do is mark `totp` as enabled for a user as soon as they have setup a device successfuly. This happens in the `verifyDevicePOST` API override as shown above. Once a device is verified, we mark the `totp` factor as enabled for the user, and the next time they login, they will be asked to complete the TOTP challenge. + +
      + +In both of the examples above, notice that we have initialised the TOTP recipe in the `recipeList`. Here are some of the configrations you can add to the `totp.init` function: +- `issuer`: This is the name that will show up in the TOTP app for the user. By default, this is equal to the `appName` config, however, you can change it to something else using this property. +- `defaultSkew`: The default value of this is `1`, which means that TOTP codes that were generated 1 tick before, and that will be generated 1 tick after from the current tick will be accepted at any given time (including the TOTP of the current tick, of course). +- `defaultPeriod`: The default value of this is `30`, which means that the current tick is valie for 30 seconds. So by default, a TOTP code that's just shown to the user, is valid for 60 seconds (`defaultPeriod + defaultSkew*defaultPeriod` seconds) + +Once the user finishes the first factor (for example, with emailpassword), their session access token payload will look like this (for those that require TOTP): +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + }, + "v": false + } +} +``` + +The `v` being `false` indicates that there are still factors that are pending. After the user has finished totp, the payload will look like: + +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "totp": 1702877999 + }, + "v": true + } +} +``` + +Indicating that the user has finished all required factors, and should be allowed to access the app. + +### Frontend setup + +There are two parts to this: +- Configuring the frontend to show the TOTP UI when required during login / sign up +- Allowing users to enable / disable TOTP on their account via the settings page (If you are following Example 2 from above). + +The first part is identical to the steps mentioned in [this section](./totp-for-all-users#frontend-setup), so please follow that. + +The second part, which is only applicable in case you want to allow users to enable / disable TOTP themselves, can be achieved by creating the following flow on your frontend: +- When the user navigates to their settings page, you can show them if TOTP is enabled or not. +- If enabled, you can show them a list of current TOTP devices with options to remove any. +- If enabled, you can show them an option to add a new TOTP device. + +In order to know if the user has enabled TOTP, you can make an API your backend which calls the following function: + + + + + + + + +```ts +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +async function isTotpEnabledForUser(userId: string) { + let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId) + return factors.includes("totp") +} +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + + + +If the user wants to enable or disable TOTP for them, you can make an API on your backend which calls the following function: + + + + +```ts +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +async function enableMFAForUser(userId: string) { + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, "totp") +} + +async function disableMFAForUser(userId: string) { + await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, "totp") +} +``` + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +:::note +Coming soon. In the meantime, checkout the [legacy method](./legacy-method/how-it-works) for adding MFA to your app. +::: + + + + +In order to list existing TOTP devices on the frontend, you can call the following API: + + + + + + + + + +```tsx +import Session from "supertokens-web-js/recipe/session" +import Totp from "supertokens-web-js/recipe/totp" + +async function fetchTOTPDevices() { + if (await Session.doesSessionExist()) { + try { + let totpDevicesResponse = await Totp.listDevices(); + for (let i = 0; i < totpDevicesResponse.devices.length; i++) { + let currDevice = totpDevicesResponse.devices[i]; + console.log(currDevice.name) // by default, this will be like "TOTP Device 1" + console.log(currDevice.verified) + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists") + } +} +``` + + + + + +```tsx +import supertokensSession from "supertokens-web-js-script/recipe/session" +import supertokensTotp from "supertokens-web-js-script/recipe/totp" +async function fetchTOTPDevices() { + if (await supertokensSession.doesSessionExist()) { + try { + let totpDevicesResponse = await supertokensTotp.listDevices(); + for (let i = 0; i < totpDevicesResponse.devices.length; i++) { + let currDevice = totpDevicesResponse.devices[i]; + console.log(currDevice.name) // by default, this will be like "TOTP Device 1" + console.log(currDevice.verified) + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists") + } +} +``` + + + + + + + + + + + +Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): + +```bash +curl --location --request GET '^{form_apiDomain}^{form_apiBasePath}/totp/device/list' \ +--header 'rid: totp' \ +--header 'Authorization: Bearer ...' +``` + + + +The output from the API call is as follows: + +```json +{ + "status": "OK", + "devices": { + "name": "TOTP Device 1", + "period": 30, + "skew": 1, + "verified": true + }[]; +} | { + "status": "GENERAL_ERROR" +} +``` + + + + + +- A `status: OK` will contain the list of all devices that exist for this user, across all of the user's tenants. We recommend only showing the devices that are `verified` to the user. +- A `status: GENERAL_ERROR`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend + +In order to remove a device, you can call the following API from the frontend: + + + + + + + + + +```tsx +import Session from "supertokens-web-js/recipe/session" +import Totp from "supertokens-web-js/recipe/totp" + +async function removeTOTPDevices(deviceName: string) { + if (await Session.doesSessionExist()) { + try { + await Totp.removeDevice({ + deviceName + }); + // device is removed + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists") + } +} +``` + + + + + +```tsx +import supertokensSession from "supertokens-web-js-script/recipe/session" +import supertokensTotp from "supertokens-web-js-script/recipe/totp" +async function removeTOTPDevices(deviceName: string) { + if (await supertokensSession.doesSessionExist()) { + try { + await supertokensTotp.removeDevice({ + deviceName + }); + // device is removed + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists") + } +} +``` + + + + + + + + + + + +Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): + +```bash +curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/totp/device/remove' \ +--header 'rid: totp' \ +--header 'Authorization: Bearer ...' +--header 'Content-Type: application/json' \ +--data-raw '{ + "deviceName": "..." +}' +``` + + + +The output from the API call is as follows: + +```json +{ + "status": "OK", + "didDeviceExist": true; +} | { + "status": "GENERAL_ERROR" +} +``` + + + + + + + + + + + + + +In order to add a new device, you can call the following function from the frontend. This function will redirect the user to the TOTP create device pre built UI. After the user has finished the new device creation and verification, they will be redirected back to the current page: + +```tsx +import MultiFactorAuth from 'supertokens-auth-react/recipe/multifactorauth'; + +async function redirectToTotpSetupScreen() { + MultiFactorAuth.redirectToFactor("totp", true, true) +} +``` + +- In the snippet above, we redirect to the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option). The second argument represents a boolean for `forceSetup` which we set to true since we want the user to setup a new TOTP device. The third arg is also `true` since we want to redirect back to the current page after the user has finished setting up the device. +- You can also just redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` if you don't want to use the above function. + + + + + + +In order to add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` from your settings page. This will show the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) to the user: +- We add the query param of `setup=true` because we want to create a new device. +- The `redirectToPath` query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device. + + + + + +After the user has finished creating a device, our backend override for `verifyDevicePOST` (see "Example 2" in [Backend setup section](#backend-setup) above) will add totp as a required factor for this user, so that next time they login, they will be asked to complete the TOTP challenge. + + + + + +In order to create a new device, you should redirect the user to a page which creates a new TOTP device on the backend, asks the user to scan the QR code and then to enter the TOTP in order to verify the new device. This can be achieved by calling the functions mentioned in [this section](./totp-for-all-users#case-1-implementation-user-needs-to-setup-a-new-totp-device--cust). + + + + + + +## Multi tenant setup + +### Backend setup +A user can be a part of several tenants. So if you want TOTP to be enabled for a specific user across all the tenants that they are a part of, the steps are the same as in the [Backend setup](#backend-setup) section above. + +However, if you want TOTP to be enabled for a specific user, for a specific tenant (or a sub set of tenants that the user is a part of), then you will have to add additional logic to the `getMFARequirementsForAuth` function override. Modifying the example code from the [Backend setup](#backend-setup) section above: + +
      Example 1: Only enable TOTP for users that have an `admin` role + + + + +```ts +import supertokens from "supertokens-node"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import totp from "supertokens-node/recipe/totp" +import Session from "supertokens-node/recipe/session" +import UserRoles from "supertokens-node/recipe/userroles" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + UserRoles.init(), + ThirdPartyEmailPassword.init({ + //... + }), + // highlight-next-line + totp.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) + // highlight-next-line + if (roles.roles.includes("admin") && await shouldRequireTotpForTenant(input.tenantId)) { + // we only want totp for admins + return ["totp"] + } else { + // no MFA for non admin users. + return [] + } + } + } + } + } + }) + ] +}) + +// highlight-start +async function shouldRequireTotpForTenant(tenantId: string): Promise { + // your logic here to determine if we care about totp for this tenant or not. + return true; +} +// highlight-end +``` + + + + +:::note +Coming soon. +::: + + + + +:::note +Coming soon. +::: + + + + +- The implementation of `shouldRequireTotpForTenant` is entirely up to you. + +
      + +
      Example 2: Ask for TOTP only for users that have enabled TOTP on their account + + + + +```ts +import supertokens from "supertokens-node"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword" +import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth" +import totp from "supertokens-node/recipe/totp" +import Session from "supertokens-node/recipe/session" + +supertokens.init({ + supertokens: { + connectionURI: "..." + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "..." + }, + recipeList: [ + Session.init(), + ThirdPartyEmailPassword.init({ + //... + }), + totp.init({ + override: { + apis: (oI) => { + return { + ...oI, + verifyDevicePOST: async function (input) { + let response = await oI.verifyDevicePOST!(input); + if (response.status === "OK") { + // device successfully verified. We save that this user has enabled TOTP in the user metadata. + // The multifactorauth recipe will pick this value up next time the user is trying to login, and + // ask them to enter the TOTP code. + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.TOTP); + } + return response; + } + } + } + } + }), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + if ((await input.requiredSecondaryFactorsForUser).includes("totp")) { + // this means that the user has finished setting up a device from their settings page. + if (await shouldRequireTotpForTenant(input.tenantId)) { + return ["totp"] + } + } + // no totp required for input.user, with the input.tenant. + return [] + } + } + } + } + // highlight-end + }) + ] +}) + +// highlight-start +async function shouldRequireTotpForTenant(tenantId: string): Promise { + // your logic here to determine if we care about totp for this tenant or not. + return true; +} +// highlight-end +``` + + + + +:::note +Coming soon. +::: + + + + +:::note +Coming soon. +::: + + + + +- We provide an override for `getMFARequirementsForAuth` which checks if TOTP is enabled for the user, and also take into account the tenantId to decide if we want to have this user go through the TOTP flow whilst logging into this tenant. The implementation of `shouldRequireTotpForTenant` is entirely up to you. + +
      + +### Frontend setup +The frontend setup is identical to the [frontend setup](#frontend-setup) section above. + + + + + +## Protecting frontend and backend routes + +See the section on [protecting frontend and backend routes](../protect-routes). + +## Frontend events, pre and post API hooks + +TODO.. + + \ No newline at end of file diff --git a/v2/mfa/with-email-verification.mdx b/v2/mfa/with-email-verification.mdx new file mode 100644 index 000000000..f220fe850 --- /dev/null +++ b/v2/mfa/with-email-verification.mdx @@ -0,0 +1,110 @@ +--- +id: with-email-verification +title: Using MFA with email verification +hide_title: true +show_ui_switcher: true +--- + +import MFAPaidBanner from '../community/reusableMD/mfa/MFAPaidBanner.mdx' +import TabItem from '@theme/TabItem'; +import CustomAdmonition from "/src/components/customAdmonition" +import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher" +import FrontendPreBuiltUITabs from "/src/components/tabs/FrontendPreBuiltUITabs" +import FrontendCustomUITabs from "/src/components/tabs/FrontendCustomUITabs" +import NpmOrScriptTabs from "/src/components/tabs/NpmOrScriptTabs" +import AppInfoForm from "/src/components/appInfoForm" +import FrontendMobileSubTabs from "/src/components/tabs/FrontendMobileSubTabs" +import {Question, Answer}from "/src/components/question" + + + +# Using MFA with email verification + +To start off, you need to add the email verification recipe to your SuperTokens setup as usual. Here are the links to the email verification recipe setup depending for each of the auth recipes we offer (The content is the same for all, it's just repeated in the individual auth recipe docs for convenience): + +- [EmailPassword](/docs/emailpassword/common-customizations/email-verification/about) +- [ThirdParty](/docs/thirdparty/common-customizations/email-verification/about) +- [Passwordless](/docs/passwordless/common-customizations/email-verification/about) +- [ThirdPartyEmailPassword](/docs/thirdpartyemailpassword/common-customizations/email-verification/about) +- [ThirdPartyPasswordless](/docs/thirdpartypasswordless/common-customizations/email-verification/about) + + + + + +:::important +This guide is only applicable for when the email verifcation mode is `REQUIRED` because for `OPTIONAL` mode, the email verification challenge is not asked by the pre built Ui during the sign up process anyway. +::: + +The default behaviour of is that for `REQUIRED` mode of email verification, the user will be asked to complete the email verification challenge first, and then all of the MFA challenges. For example, if the user has emailpassword as the first factor, and then TOTP as a second factor, then SuperTokens will ask the user to do email password login, followed by email verification, followed by TOTP. + +If you want to switch the order where email verification happens after the secondary factors of MFA, then you should do the following: + + + + + +```tsx +import supertokens from "supertokens-auth-react" +import EmailVerification from "supertokens-auth-react/recipe/emailverification"; +import Session from "supertokens-auth-react/recipe/session"; + +supertokens.init({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // other recipes... + EmailVerification.init({ + mode: "REQUIRED", + }), + Session.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // highlight-start + getGlobalClaimValidators: (input) => { + let emailVerificationClaimValidator = input.claimValidatorsAddedByOtherRecipes.find(v => v.id === EmailVerification.EmailVerificationClaim.id)!; + let filteredValidators = input.claimValidatorsAddedByOtherRecipes.filter(v => v.id !== EmailVerification.EmailVerificationClaim.id); + return [...filteredValidators, emailVerificationClaimValidator]; + } + // highlight-end + } + } + } + }) + ] +}) +``` + +In the snippet above, we override the `getGlobalClaimValidators` function in the Session recipe to add the email verification validator at the end of the returned validators array. This ensures that post the first factor sign up, the first validator that fails is the MFA one which will redirect the user to complete the MFA factors. + + + + + + + + + +For custom UI, since you control the routing of the app, it's entirely up to you as to when you want to ask the user to verify their email during the sign up process. Whilst the user's email is in an unverified state, the invalid claims array returned from a call to `await Session.validateClaims` will contain the email verification ID along with other invalid claims. The order in which you want to resolve each is up to you. + + + + + + +:::caution +If you are using otp-email MFA factor as a form of email verification, you should also have emailverification recipe initialised in `REQUIRED` mode on the backend (no need to add it on the frontend since users won't see that UI). This is for security reasons wherein during the sign up process, when asking for the otp-email challenge, the email the OTP is sent to is decided on the frontend (automatically). In this case, the following scenario is possible: +- User signs up with email `A` +- An email OTP challenge is shown to the user, and an OTP to email `A` is sent automatically. +- The user manually calls the OTP create code API with email `B` and their session token, and verifies the OTP via a call to the consume code API. +- The user refreshes the page and the otp-email challenge is completed. + +Now of course, this is not the desired flow when you want to use otp-email as a form of email verification. To prevent this, you should have the emailverification recipe initialised in `REQUIRED` mode on the backend. This will ensure that the email verification claim validator will only pass if the email that's verified is the one from the first factor (email `A`). + +The above case is only possible during sign up, and not sign in. +::: \ No newline at end of file diff --git a/v2/passwordless/advanced-customizations/frontend-hooks/handle-event.mdx b/v2/passwordless/advanced-customizations/frontend-hooks/handle-event.mdx index 83d32e6f4..4b3cbf2e8 100644 --- a/v2/passwordless/advanced-customizations/frontend-hooks/handle-event.mdx +++ b/v2/passwordless/advanced-customizations/frontend-hooks/handle-event.mdx @@ -28,11 +28,15 @@ Passwordless.init({ // called when a user visits the login / sign up page with a valid session // in this case, they are usually redirected to the main app } else if (context.action === "SUCCESS") { - let user = context.user; - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // sign up success + if (context.createdNewSession) { + let user = context.user; + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // sign up success + } else { + // sign in success + } } else { - // sign in success + // this is during second factor login } } }, diff --git a/v2/passwordless/advanced-customizations/frontend-hooks/redirection-callback.mdx b/v2/passwordless/advanced-customizations/frontend-hooks/redirection-callback.mdx index 763edf274..396c9d1e0 100644 --- a/v2/passwordless/advanced-customizations/frontend-hooks/redirection-callback.mdx +++ b/v2/passwordless/advanced-customizations/frontend-hooks/redirection-callback.mdx @@ -16,18 +16,23 @@ This function is used to change where the user is redirected to post certain act ```tsx import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import SuperTokens from "supertokens-auth-react" -Passwordless.init({ - contactMethod: "EMAIL", // This example will work with any contactMethod. +SuperTokens.init({ + appInfo: { + appName: "SuperTokens", + apiDomain: "http://localhost:3000", + websiteDomain: "http://localhost:3000", + }, getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { + if (context.action === "SUCCESS" && context.newSessionCreated) { // called on a successful sign in / up. Where should the user go next? let redirectToPath = context.redirectToPath; if (redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return redirectToPath; } - if (context.isNewPrimaryUser) { + if (context.createdNewUser) { // user signed up return "/onboarding" } else { @@ -37,7 +42,10 @@ Passwordless.init({ } // return undefined to let the default behaviour play out return undefined; - } + }, + recipeList: [Passwordless.init({ + contactMethod: "EMAIL", // This example will work with any contactMethod. + })], }); ``` diff --git a/v2/passwordless/advanced-customizations/user-context.mdx b/v2/passwordless/advanced-customizations/user-context.mdx index 60962e05a..e0d374130 100644 --- a/v2/passwordless/advanced-customizations/user-context.mdx +++ b/v2/passwordless/advanced-customizations/user-context.mdx @@ -55,7 +55,7 @@ SuperTokens.init({ ...originalImplementation, consumeCode: async function (input) { let resp = await originalImplementation.consumeCode(input); - if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1) { + if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1 && input.session === undefined) { /* * This is called during the consume code API, * but before calling the createNewSession function. diff --git a/v2/passwordless/common-customizations/account-linking/adding-accounts-to-session.mdx b/v2/passwordless/common-customizations/account-linking/adding-accounts-to-session.mdx new file mode 100644 index 000000000..d48354bf0 --- /dev/null +++ b/v2/passwordless/common-customizations/account-linking/adding-accounts-to-session.mdx @@ -0,0 +1,347 @@ +--- +id: adding-accounts-to-session +title: Linking social accounts or adding a password to an existing account +hide_title: true +--- + +import AccountLinkingPaidBanner from '../../../community/reusableMD/accountlinking/AccountLinkingPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Linking social accounts or adding a password to an existing account + +There may be scenarios in which you want to link a social account to an existing user account, or add a password to an account that was created using a social provider (or passwordless login). This guide will walk you through how to do this. + +The idea here is that we reuse the existing sign up APIs, but call them with a session's access token. The APIs will then create a new recipe user for that login method based on the input, and then link that to the session user. Of course, there are security checks done to ensure there is no account takeover risk, and we will go through them in this guide as well. + +:::caution +We do not provide pre built UIs for this flow since it's probably something you want to add in your settings page or during the sign up process, so this guide will focus on which APIs to call from your own UI. + +The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised our `supertokens-auth-react` SDK, on the frontend. +::: + +## Linking a social account to an existing user account + +### Step 1: Enable account linking and third party on the backend SDK + + + + +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +supertokens.init({ + supertokens: { + connectionURI: "...", + apiKey: "..." + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + ThirdParty.init({ /* ...*/ }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (user === undefined) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + if (session !== undefined && session.getUserId() === user.id) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + return { + shouldAutomaticallyLink: false + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +In the above implementation of `shouldDoAutomaticAccountLinking`, we only allow account linking if the input session is present. This means that we are trying to link a social login account to an existing session user. Otherwise, we do not allow account linking which means that first factor account linking is disabled. If you want to enable that too, you can see [this page](./automatic-account-linking). + +Read the [third party recipe docs](/docs/thirdparty/common-customizations/sign-in-and-up/built-in-providers#step-2-adding-providers-config-to-the-backend) to learn how to add provider config. + +### Step 2: Create a UI to show social login buttons and handle login + +First, you will need to detect which social login methods are already linked to the user. This can be done by inspecting the [user object](../../user-object) on the backend and checking the `thirdParty.id` property (the values will be like `google`, `facebook` etc). + +Then you will have to create your own UI which asks the user to pick a social login provider to connect to. Once they click on one, you will redirect them to that provider's page. Post login, the provider will redirect the user back to your application (on the same path as the first factor login) after which you will call our APIs to consume the OAuth tokens and link the user. + +The exact implementation of the above can be found [here](/docs/thirdparty/custom-ui/thirdparty-login). The two big differences in the implementation are: +- When you call the signinup API, you need to provide the session's access token in the request. If you are using our frontend SDK, this is done automatically via our frontend network interceptors. The access token will enable the backend to get a session and then link the social login account to session user. +- There are new types of failure scenarios when calling the signinup API which are not possible during first factor login. To learn more about them, see the [error codes section](/docs/thirdparty/common-customizations/account-linking/automatic-account-linking#err_code_001) (> `ERR_CODE_008`). + +### Step 3: Extract the social login access token and user peofile info on the backend + +Once you call the signinup API from the frontend, SuperTokens will verify the OAuth tokens and fetch the user's profile info from the third party provider. SuperTokens will also link the newly created recipe user to the session user. + +To fetch the new user object and also the third party profile, you can override the signinup recipe function: + + + + +```tsx +import SuperTokens, { User } from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // override the thirdparty sign in / up function + signInUp: async function (input) { + + let existingUser: User | undefined = undefined; + if (input.session !== undefined) { + existingUser = await SuperTokens.getUser(input.session.getUserId()); + } + + let response = await originalImplementation.signInUp(input); + + if (response.status === "OK") { + + let accessToken = response.oAuthTokens["access_token"]; + + let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; + + if (input.session !== undefined && response.user.id === input.session.getUserId()) { + if (response.user.loginMethods.length === existingUser!.loginMethods.length + 1) { + // new social account was linked to session user + } else { + // social account was already linked to the session + // user from before + } + } + } + + return response; + } + } + } + } + // highlight-end + }), + Session.init({ /* ... */ }) + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +Notice in the above snippet that we check for `input.session !== undefined && response.user.id === input.session.getUserId()`. This ensures that we run our custom logic only if it's linking a social account to your session account, and not during first factor login. + +## Adding a password to an existing account + +### Step 1: Enable account linking and emailpassword on the backend SDK + + + + +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; +import EmailPassword from "supertokens-node/recipe/emailpassword" + +supertokens.init({ + supertokens: { + connectionURI: "...", + apiKey: "..." + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + EmailPassword.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (user === undefined) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + if (session !== undefined && session.getUserId() === user.id) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + return { + shouldAutomaticallyLink: false + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +In the above implementation of `shouldDoAutomaticAccountLinking`, we only allow account linking if the input session is present. This means that we are trying to link an email password account to an existing session user. Otherwise, we do not allow account linking which means that first factor account linking is disabled. If you want to enable that too, you can see [this page](./automatic-account-linking). + +### Step 2: Create a UI to show a password input to the user and handle the submit event + +:::important +If you want to use password based auth as a second factor, or for step up auth, see our docs in the [MFA recipe](/docs/mfa/introduction) instead. The guide below is only meant for if you want to add a password for a user and allow them to login via email password for first factor login. +::: + +First, you will need to detect if there already exists a password for the user. This can be done by inspecting the [user object](../../user-object) on the backend and checking if there is an `emailpassword` login method. + +Then, if no such login method exists, you will have to show a UI in which the user can add a password to their account. The default password validation rules can be found [here](/docs/emailpassword/common-customizations/signup-form/field-validators#changing-the-default-email-and-password-validators). + +You will also need to fetch the email of the user before you call the email password sign up API. Once again, you can fetch this using the the user object. If the user object does not have an email (which can only happen if the first factor is phone OTP), then you should ask the user to go through an email OTP flow (via out passwordless recipe) before asking them to set a password. Thge email OTP flow will also result in a passwordless user account being created and linked to the session user. + +Once you have the email on the frontend, you should call the [sign up API](/docs/emailpassword/custom-ui/email-password-login#sign-up-form). The two big differences in the implementation are: +- When you call the signup API, you need to provide the session's access token in the request. If you are using our frontend SDK, this is done automatically via our frontend network interceptors. The access token will enable the backend to get a session and then link the email password account to session user. +- There are new types of failure scenarios when calling the signup API which are not possible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#err_code_001) (> `ERR_CODE_008`). + +### Step 3: Checking for email match in the backend sign up API +Since the email is specified on the frontend, we want to verify it in the backend API before using it (since we shouldn't trust the frontend). This can be easily done by overriding the email password sign up API: + + + + +```tsx +import SuperTokens from "supertokens-node"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + // highlight-start + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signUpPOST: async function (input) { + if (input.session !== undefined) { + // this means that we are trying to add a password to the session user + const inputEmail = input.formFields.find(f => f.id === "email")!.value; + let sessionUserId = input.session.getUserId(); + let userObject = await SuperTokens.getUser(sessionUserId); + if (userObject!.emails.find(e => e === inputEmail) === undefined) { + // this means that the input email does not belong to this user. + return { + status: "GENERAL_ERROR", + message: "Cannot use this email to add a password for this user" + } + } + } + return await originalImplementation.signUpPOST!(input); + } + } + } + } + // highlight-end + }), + Session.init({ /* ... */ }) + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + \ No newline at end of file diff --git a/v2/passwordless/common-customizations/change-magic-link-url.mdx b/v2/passwordless/common-customizations/change-magic-link-url.mdx index edfacfcd9..97bb9dc23 100644 --- a/v2/passwordless/common-customizations/change-magic-link-url.mdx +++ b/v2/passwordless/common-customizations/change-magic-link-url.mdx @@ -44,17 +44,14 @@ SuperTokens.init({ return { ...originalImplementation, sendEmail: async function (input) { - if (input.type === "PASSWORDLESS_LOGIN") { - return originalImplementation.sendEmail({ - ...input, - urlWithLinkCode: input.urlWithLinkCode?.replace( - // This is: `${websiteDomain}${websiteBasePath}/verify` - "http://localhost:3000/auth/verify", - "http://your.domain.com/your/path" - ) - }) - } - return originalImplementation.sendEmail(input); + return originalImplementation.sendEmail({ + ...input, + urlWithLinkCode: input.urlWithLinkCode?.replace( + // This is: `${websiteDomain}${websiteBasePath}/verify` + "http://localhost:3000/auth/verify", + "http://your.domain.com/your/path" + ) + }) } } } diff --git a/v2/passwordless/common-customizations/embed-sign-in-up-form.mdx b/v2/passwordless/common-customizations/embed-sign-in-up-form.mdx index b51d8882e..9cbf732ed 100644 --- a/v2/passwordless/common-customizations/embed-sign-in-up-form.mdx +++ b/v2/passwordless/common-customizations/embed-sign-in-up-form.mdx @@ -56,21 +56,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start - async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -120,21 +115,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start - async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -186,21 +176,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start - async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -226,8 +211,7 @@ function AuthPage() { In the above code snippet, we: 1. Disabled the default Auth UI by setting `disableDefaultUI` to `true` inside the Passwordless recipe config. -2. Overrode the `getRedirectionURL` function inside the Passwordless recipe config to redirect to `/dashboard` upon successful login. -3. Overrode the `getRedirectionURL` function inside the SuperTokens config to redirect to `/auth` when login is required. +2. Override the `getRedirectionURL` function inside the SuperTokens config to redirect to `/auth` when login is required and to redirect to `/dashboard` upon successful login. Feel free to customize the redirection URLs as needed. diff --git a/v2/passwordless/common-customizations/handling-signinup-success.mdx b/v2/passwordless/common-customizations/handling-signinup-success.mdx index ee94fd3fe..eaaf6317b 100644 --- a/v2/passwordless/common-customizations/handling-signinup-success.mdx +++ b/v2/passwordless/common-customizations/handling-signinup-success.mdx @@ -125,10 +125,12 @@ SuperTokens.init({ if (response.status === "OK") { let { id, emails, phoneNumbers } = response.user; - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // TODO: post sign up logic - } else { - // TODO: post sign in logic + if (input.session === undefined) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // TODO: post sign up logic + } else { + // TODO: post sign in logic + } } } return response; diff --git a/v2/passwordless/common-customizations/multi-tenancy/common-domain-login.mdx b/v2/passwordless/common-customizations/multi-tenancy/common-domain-login.mdx index 2593c2978..5c43bbced 100644 --- a/v2/passwordless/common-customizations/multi-tenancy/common-domain-login.mdx +++ b/v2/passwordless/common-customizations/multi-tenancy/common-domain-login.mdx @@ -285,7 +285,10 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.passwordless.enabled) { + if (loginMethods.firstFactors.includes("otp-email") || + loginMethods.firstFactors.includes("otp-sms") || + loginMethods.firstFactors.includes("link-email") || + loginMethods.firstFactors.includes("link-sms")) { // passwordless login is enabled for the tenant } else { // check for other login methods being enabled for the tenant. @@ -303,7 +306,10 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.passwordless.enabled) { + if (loginMethods.firstFactors.includes("otp-email") || + loginMethods.firstFactors.includes("otp-sms") || + loginMethods.firstFactors.includes("link-email") || + loginMethods.firstFactors.includes("link-sms")) { // passwordless login is enabled for the tenant } else { // check for other login methods being enabled for the tenant. @@ -642,23 +648,25 @@ SuperTokens.init({ appName: "...", websiteDomain: "...", }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + let claimValue: string[] | undefined = await Session.getClaimValue({ + claim: Multitenancy.AllowedDomainsClaim + }); + if (claimValue !== undefined) { + window.location.href = "https://" + claimValue[0]; + } else { + // there was no configured allowed domain for this user. Throw an error cause of + // misconfig or redirect to a default sub domain + } + } + return undefined; + }, + // highlight-end recipeList: [ ^{recipeNameCapitalLetters}.init({ ^{reactRecipeInitDefault} - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { - let claimValue: string[] | undefined = await Session.getClaimValue({ - claim: Multitenancy.AllowedDomainsClaim - }); - if (claimValue !== undefined) { - window.location.href = "https://" + claimValue[0]; - } else { - // there was no configured allowed domain for this user. Throw an error cause of - // misconfig or redirect to a default sub domain - } - } - return undefined; - } }), ] }); diff --git a/v2/passwordless/common-customizations/multi-tenancy/sub-domain-login.mdx b/v2/passwordless/common-customizations/multi-tenancy/sub-domain-login.mdx index 673e21dd6..0cb0bbb08 100644 --- a/v2/passwordless/common-customizations/multi-tenancy/sub-domain-login.mdx +++ b/v2/passwordless/common-customizations/multi-tenancy/sub-domain-login.mdx @@ -158,7 +158,10 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.passwordless.enabled) { + if (loginMethods.firstFactors.includes("otp-email") || + loginMethods.firstFactors.includes("otp-sms") || + loginMethods.firstFactors.includes("link-email") || + loginMethods.firstFactors.includes("link-sms")) { // passwordless login is enabled for the tenant } else { // check for other login methods being enabled for the tenant. @@ -176,7 +179,10 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.passwordless.enabled) { + if (loginMethods.firstFactors.includes("otp-email") || + loginMethods.firstFactors.includes("otp-sms") || + loginMethods.firstFactors.includes("link-email") || + loginMethods.firstFactors.includes("link-sms")) { // passwordless login is enabled for the tenant } else { // check for other login methods being enabled for the tenant. diff --git a/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx b/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx index 8900a22f7..545e51ca3 100644 --- a/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/passwordless/common-customizations/sessions/claims/claim-validators.mdx @@ -52,17 +52,20 @@ Without a special construct of session claim validators, the updating of the ses Before we dive deep into claim validators, let's talk about session claim objects. These are objects that conform to an interface that allows SuperTokens to automatically add session claims to the access token payload. Here is the interface: ```tsx + +import { RecipeUserId } from "supertokens-node"; + type JSONObject = any; // typecheck-only, removed from output interface SessionClaim { readonly key: string; - fetchValue(userId: string, tenantId: string, userContext: any): Promise; + fetchValue(userId: string, recipeUserId: RecipeUserId, tenantId: string, currentPayload: JSONObject | undefined, userContext: any): Promise; addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject; - removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayloadByMerge_internal(payload: JSONObject, userContext: any): JSONObject; - removeFromPayload(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayload(payload: JSONObject, userContext: any): JSONObject; getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; @@ -364,7 +367,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)) }; /* @@ -509,8 +512,8 @@ The `build` function is a helper function which all claims have that does the fo ```text class Claim { // other functions like fetchValue, getValueFromPayload etc.. - function build(userId, tenantId) { - claimValue = this.fetchValue(userId, tenantId); + function build(userId, recipeUserId, tenantId) { + claimValue = this.fetchValue(userId, recipeUserId, tenantId); return this.addToPayload_internal({}, claimValue) } } diff --git a/v2/passwordless/common-customizations/sessions/fetch-sessions-for-user.mdx b/v2/passwordless/common-customizations/sessions/fetch-sessions-for-user.mdx index 989d8c1cb..cfec26595 100644 --- a/v2/passwordless/common-customizations/sessions/fetch-sessions-for-user.mdx +++ b/v2/passwordless/common-customizations/sessions/fetch-sessions-for-user.mdx @@ -121,6 +121,6 @@ for session_handle in session_handles: -By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to delete the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. +By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to fetch the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. diff --git a/v2/passwordless/common-customizations/sessions/jwt-signing-key-rotation.mdx b/v2/passwordless/common-customizations/sessions/jwt-signing-key-rotation.mdx index d5a198254..dd357fd96 100644 --- a/v2/passwordless/common-customizations/sessions/jwt-signing-key-rotation.mdx +++ b/v2/passwordless/common-customizations/sessions/jwt-signing-key-rotation.mdx @@ -90,6 +90,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -114,6 +119,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -134,9 +152,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -147,7 +162,10 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + diff --git a/v2/passwordless/common-customizations/sessions/revoke-session.mdx b/v2/passwordless/common-customizations/sessions/revoke-session.mdx index 3b7acdf47..c474e8976 100644 --- a/v2/passwordless/common-customizations/sessions/revoke-session.mdx +++ b/v2/passwordless/common-customizations/sessions/revoke-session.mdx @@ -46,7 +46,7 @@ function NavBar() { async function onLogout() { // highlight-next-line await signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } return (
        @@ -71,7 +71,7 @@ import Session from "supertokens-auth-react/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -101,7 +101,7 @@ import Session from "supertokens-web-js/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -113,7 +113,7 @@ import supertokensSession from "supertokens-web-js-script/recipe/session"; async function logout () { // highlight-next-line await supertokensSession.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` diff --git a/v2/passwordless/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/passwordless/common-customizations/sessions/with-jwt/jwt-verification.mdx index c986e3b97..d1f0e905a 100644 --- a/v2/passwordless/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/passwordless/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -170,6 +170,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -194,6 +199,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -214,9 +232,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -227,9 +242,12 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + diff --git a/v2/passwordless/common-customizations/userid-format.mdx b/v2/passwordless/common-customizations/userid-format.mdx index 1f9d41cd9..8578b13d7 100644 --- a/v2/passwordless/common-customizations/userid-format.mdx +++ b/v2/passwordless/common-customizations/userid-format.mdx @@ -58,7 +58,7 @@ SuperTokens.init({ if (response.status === "OK") { let { id, emails, phoneNumbers } = response.user; - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1 && input.session === undefined) { // highlight-start let externalUserId = "" await SuperTokens.createUserIdMapping({ superTokensUserId: response.user.id, externalUserId }) diff --git a/v2/passwordless/custom-ui/login-magic-link.mdx b/v2/passwordless/custom-ui/login-magic-link.mdx index 64bfe0ffb..3d4c288f4 100644 --- a/v2/passwordless/custom-ui/login-magic-link.mdx +++ b/v2/passwordless/custom-ui/login-magic-link.mdx @@ -54,7 +54,10 @@ async function sendMagicLink(email: string) { */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. See that section in our docs. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // Magic link sent successfully. window.alert("Please check your email for the magic link"); @@ -91,7 +94,10 @@ async function sendMagicLink(email: string) { */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. See that section in our docs. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // Magic link sent successfully. window.alert("Please check your email for the magic link"); @@ -143,7 +149,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup/co The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the magic link was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend, or if the input email or password failed the backend validation logic. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. The response from the API call is the following object (in case of `status: "OK"`): ```json @@ -196,17 +202,14 @@ SuperTokens.init({ return { ...originalImplementation, sendEmail: async function (input) { - if (input.type === "PASSWORDLESS_LOGIN") { - return originalImplementation.sendEmail({ - ...input, - urlWithLinkCode: input.urlWithLinkCode?.replace( - // This is: `${websiteDomain}${websiteBasePath}/verify` - "http://localhost:3000/auth/verify", - "http://your.domain.com/your/path" - ) - }) - } - return originalImplementation.sendEmail(input); + return originalImplementation.sendEmail({ + ...input, + urlWithLinkCode: input.urlWithLinkCode?.replace( + // This is: `${websiteDomain}${websiteBasePath}/verify` + "http://localhost:3000/auth/verify", + "http://your.domain.com/your/path" + ) + }) } } } @@ -633,7 +636,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" | "RESTART_FLOW_ERROR"`: These responses indicate that the Magic link was invalid or expired. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. diff --git a/v2/passwordless/custom-ui/login-otp.mdx b/v2/passwordless/custom-ui/login-otp.mdx index 9a978d7b9..1078880a8 100644 --- a/v2/passwordless/custom-ui/login-otp.mdx +++ b/v2/passwordless/custom-ui/login-otp.mdx @@ -53,7 +53,10 @@ async function sendOTP(email: string) { */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. See that section in our docs. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // OTP sent successfully. window.alert("Please check your email for an OTP"); @@ -90,7 +93,10 @@ async function sendOTP(email: string) { */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. See that section in our docs. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // OTP sent successfully. window.alert("Please check your email for an OTP"); @@ -142,7 +148,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup/co The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the OTP was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend, or if the input email or password failed the backend validation logic. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. The response from the API call is the following object (in case of `status: "OK"`): ```json @@ -452,7 +458,7 @@ The response body from the API call has a `status` property in it: - `status: "EXPIRED_USER_INPUT_CODE_ERROR"`: The entered OTP is too old. You should ask the user to resend a new OTP and try again. - `status: "RESTART_FLOW_ERROR"`: These responses that the user tried invalid OTPs too many times. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. diff --git a/v2/passwordless/custom-ui/sign-out.mdx b/v2/passwordless/custom-ui/sign-out.mdx index c851395f9..af9169fa0 100644 --- a/v2/passwordless/custom-ui/sign-out.mdx +++ b/v2/passwordless/custom-ui/sign-out.mdx @@ -28,7 +28,7 @@ import Session from "supertokens-web-js/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` @@ -40,7 +40,7 @@ import supertokensSession from "supertokens-web-js-script/recipe/session"; async function logout () { // highlight-next-line await supertokensSession.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` diff --git a/v2/passwordless/email-delivery/custom-method.mdx b/v2/passwordless/email-delivery/custom-method.mdx index bd3ba827b..0a0d5b310 100644 --- a/v2/passwordless/email-delivery/custom-method.mdx +++ b/v2/passwordless/email-delivery/custom-method.mdx @@ -37,12 +37,21 @@ supertokens.init({ return { ...originalImplementation, sendEmail: async function ({ + isFirstFactor, codeLifetime, // amount of time the code is alive for (in MS) email, urlWithLinkCode, // magic link userInputCode, // OTP }) { - // TODO: create and send email + if (isFirstFactor) { + // this is for first factor login + // TODO: create and send email + } else { + // this is for MFA login (only applicable if you are using MFA). + // In this case, the urlWithLinkCode will always be undefined since + // we only support OTP based MFA and not link based MFA + // TODO: create and send email + } } } } diff --git a/v2/passwordless/email-delivery/smtp/change-email-content.mdx b/v2/passwordless/email-delivery/smtp/change-email-content.mdx index 6409d67f5..aeed4104f 100644 --- a/v2/passwordless/email-delivery/smtp/change-email-content.mdx +++ b/v2/passwordless/email-delivery/smtp/change-email-content.mdx @@ -44,17 +44,30 @@ supertokens.init({ return { ...originalImplementation, getContent: async function ({ + isFirstFactor, codeLifetime, // amount of time the code is alive for (in MS) email, urlWithLinkCode, // magic link userInputCode, // OTP }) { - // send some custom email content - return { - body: "EMAIL BODY", - isHtml: true, - subject: "Some subject", - toEmail: email + if (isFirstFactor) { + // this is for first factor login + return { + body: "EMAIL BODY", + isHtml: true, + subject: "Login to your account", + toEmail: email + } + } else { + // this is for MFA login (only applicable if you are using MFA). + // In this case, the urlWithLinkCode will always be undefined since + // we only support OTP based MFA and not link based MFA + return { + body: "EMAIL BODY", + isHtml: true, + subject: "Login via MFA", + toEmail: email + } } // You can even call the original implementation and diff --git a/v2/passwordless/mfa.mdx b/v2/passwordless/mfa.mdx new file mode 100644 index 000000000..1c72b293a --- /dev/null +++ b/v2/passwordless/mfa.mdx @@ -0,0 +1,9 @@ +--- +id: mfa +title: Multi factor auth +hide_title: true +--- + +# Multi factor auth + +See our guide for Multi Factor Auth [here](/docs/mfa/introduction). \ No newline at end of file diff --git a/v2/passwordless/migration/about.mdx b/v2/passwordless/migration/about.mdx index ed9b1b976..08b0ef756 100644 --- a/v2/passwordless/migration/about.mdx +++ b/v2/passwordless/migration/about.mdx @@ -45,3 +45,6 @@ There are 3 steps to user migration: - This will prevent users from having to re-authenticate. You can learn more about how to implement these changes in the [Session Migration](./session-migration) section. + +## Step 4) MFA migration +If you are using MFA in your app, checkout the MFA migration section [here](/docs/mfa/migration) after you have gone through the above migration steps. diff --git a/v2/passwordless/migration/mfa-migration.mdx b/v2/passwordless/migration/mfa-migration.mdx new file mode 100644 index 000000000..85f7c646b --- /dev/null +++ b/v2/passwordless/migration/mfa-migration.mdx @@ -0,0 +1,12 @@ +--- +id: mfa-migration +title: Step 4) MFA migration +hide_title: true +--- + + + + +# MFA migration + +If you are using MFA in your app, checkout the MFA migration section [here](/docs/mfa/migration) after you have gone through the previous steps in migration. diff --git a/v2/passwordless/multi-tenant.mdx b/v2/passwordless/multi-tenant.mdx new file mode 100644 index 000000000..42d32e856 --- /dev/null +++ b/v2/passwordless/multi-tenant.mdx @@ -0,0 +1,9 @@ +--- +id: multi-tenant +title: Multi tenancy / B2B orgs +hide_title: true +--- + +# Multi tenancy / B2B orgs + +See our guide for multi tenancy auth [here](/docs/multitenancy/introduction). \ No newline at end of file diff --git a/v2/passwordless/nextjs/app-directory/setting-up-frontend.mdx b/v2/passwordless/nextjs/app-directory/setting-up-frontend.mdx index 31028dab7..68c551eac 100644 --- a/v2/passwordless/nextjs/app-directory/setting-up-frontend.mdx +++ b/v2/passwordless/nextjs/app-directory/setting-up-frontend.mdx @@ -62,7 +62,9 @@ export default function Auth() { ## 3) Visit `/auth` page on your website -If you see a login UI, then you have successfully completed this step! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) +If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre built UI, for each page on [this link](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/auth-page--playground). + +If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/v2/passwordless/nextjs/setting-up-frontend.mdx b/v2/passwordless/nextjs/setting-up-frontend.mdx index 9f7cf1295..e1f0a9e94 100644 --- a/v2/passwordless/nextjs/setting-up-frontend.mdx +++ b/v2/passwordless/nextjs/setting-up-frontend.mdx @@ -59,7 +59,9 @@ export default function Auth() { ## 3) Visit `/auth` page on your website -If you see a login UI, then you have successfully completed this step! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) +If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre built UI, for each page on [this link](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/auth-page--playground). + +If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/v2/passwordless/pre-built-ui/auth-redirection.mdx b/v2/passwordless/pre-built-ui/auth-redirection.mdx index c5ca52af0..7495a1784 100644 --- a/v2/passwordless/pre-built-ui/auth-redirection.mdx +++ b/v2/passwordless/pre-built-ui/auth-redirection.mdx @@ -30,26 +30,26 @@ SuperTokens.init({ appName: "...", websiteDomain: "...", }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + if (context.redirectToPath !== undefined) { + // we are navigating back to where the user was before they authenticated + return context.redirectToPath; + } + if (context.createdNewUser) { + // user signed up + } else { + // user signed in + } + return "/dashboard"; + } + return undefined; + }, + // highlight-end recipeList: [ ^{recipeNameCapitalLetters}.init({ ^{reactRecipeInitDefault} // typecheck-only, removed from output - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { - if (context.redirectToPath !== undefined) { - // we are navigating back to where the user was before they authenticated - return context.redirectToPath; - } - if (context.isNewPrimaryUser) { - // user signed up - } else { - // user signed in - } - return "/dashboard"; - } - return undefined; - } - // highlight-end }), ] }); diff --git a/v2/passwordless/pre-built-ui/sign-out.mdx b/v2/passwordless/pre-built-ui/sign-out.mdx index 6cf670151..ed1a43dca 100644 --- a/v2/passwordless/pre-built-ui/sign-out.mdx +++ b/v2/passwordless/pre-built-ui/sign-out.mdx @@ -26,7 +26,7 @@ function NavBar() { async function onLogout() { // highlight-next-line await signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } return (
          @@ -51,7 +51,7 @@ import Session from "supertokens-auth-react/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` diff --git a/v2/passwordless/sidebars.js b/v2/passwordless/sidebars.js index bdf69ed0a..827ba196f 100644 --- a/v2/passwordless/sidebars.js +++ b/v2/passwordless/sidebars.js @@ -448,6 +448,13 @@ module.exports = { "common-customizations/get-user-info", "common-customizations/user-pagination", "common-customizations/delete-user", + { + type: "category", + label: "Account Linking", + items: [ + "common-customizations/account-linking/adding-accounts-to-session" + ] + }, "common-customizations/change-magic-link-url", "common-customizations/change-code-lifetime", "common-customizations/change-email", @@ -652,6 +659,8 @@ module.exports = { "user-roles/delete-roles", ], }, + "mfa", + "multi-tenant" ] }, "rate-limits", @@ -679,7 +688,8 @@ module.exports = { ], }, "migration/data-migration", - "migration/session-migration" + "migration/session-migration", + "migration/mfa-migration" ], }, { diff --git a/v2/passwordless/sms-delivery/twilio/change-sms-content.mdx b/v2/passwordless/sms-delivery/twilio/change-sms-content.mdx index b6eaf61ff..a4e610739 100644 --- a/v2/passwordless/sms-delivery/twilio/change-sms-content.mdx +++ b/v2/passwordless/sms-delivery/twilio/change-sms-content.mdx @@ -43,15 +43,25 @@ supertokens.init({ return { ...originalImplementation, getContent: async function ({ + isFirstFactor, codeLifetime, // amount of time the code is alive for (in MS) phoneNumber, urlWithLinkCode, // magic link userInputCode, // OTP }) { - // send some custom SMS content - return { - toPhoneNumber: phoneNumber, - body: "SMS BODY" + if (isFirstFactor) { + // send some custom SMS content + return { + toPhoneNumber: phoneNumber, + body: "SMS BODY" + } + } else { + // for second factor, urlWithLinkCode will always be + // undefined since we only support OTP based for second factor + return { + toPhoneNumber: phoneNumber, + body: "SMS BODY" + } } // You can even call the original implementation and diff --git a/v2/passwordless/supabase-intergration/backend-signup-override.mdx b/v2/passwordless/supabase-intergration/backend-signup-override.mdx index 56445160a..36e020e2e 100644 --- a/v2/passwordless/supabase-intergration/backend-signup-override.mdx +++ b/v2/passwordless/supabase-intergration/backend-signup-override.mdx @@ -51,7 +51,7 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.consumeCodePOST(input); - if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1 && input.session === undefined) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); diff --git a/v2/phonepassword/backend/passwordless-customisation.mdx b/v2/phonepassword/backend/passwordless-customisation.mdx index d69b8b8d0..987d5cb4d 100644 --- a/v2/phonepassword/backend/passwordless-customisation.mdx +++ b/v2/phonepassword/backend/passwordless-customisation.mdx @@ -211,7 +211,7 @@ supertokens.init({ ...input, accessTokenPayload: { ...input.accessTokenPayload, - ...PhoneVerifiedClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext), + ...PhoneVerifiedClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext), phoneNumber: userInfo?.emails[0], }, }); diff --git a/v2/phonepassword/backend/session-customisation.mdx b/v2/phonepassword/backend/session-customisation.mdx index a337bf98c..4fa6110e5 100644 --- a/v2/phonepassword/backend/session-customisation.mdx +++ b/v2/phonepassword/backend/session-customisation.mdx @@ -52,7 +52,7 @@ supertokens.init({ ...input, accessTokenPayload: { ...input.accessTokenPayload, - ...PhoneVerifiedClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext), + ...PhoneVerifiedClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext), phoneNumber: userInfo?.emails[0], }, }); diff --git a/v2/session/common-customizations/sessions/claims/claim-validators.mdx b/v2/session/common-customizations/sessions/claims/claim-validators.mdx index 8900a22f7..545e51ca3 100644 --- a/v2/session/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/session/common-customizations/sessions/claims/claim-validators.mdx @@ -52,17 +52,20 @@ Without a special construct of session claim validators, the updating of the ses Before we dive deep into claim validators, let's talk about session claim objects. These are objects that conform to an interface that allows SuperTokens to automatically add session claims to the access token payload. Here is the interface: ```tsx + +import { RecipeUserId } from "supertokens-node"; + type JSONObject = any; // typecheck-only, removed from output interface SessionClaim { readonly key: string; - fetchValue(userId: string, tenantId: string, userContext: any): Promise; + fetchValue(userId: string, recipeUserId: RecipeUserId, tenantId: string, currentPayload: JSONObject | undefined, userContext: any): Promise; addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject; - removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayloadByMerge_internal(payload: JSONObject, userContext: any): JSONObject; - removeFromPayload(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayload(payload: JSONObject, userContext: any): JSONObject; getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; @@ -364,7 +367,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)) }; /* @@ -509,8 +512,8 @@ The `build` function is a helper function which all claims have that does the fo ```text class Claim { // other functions like fetchValue, getValueFromPayload etc.. - function build(userId, tenantId) { - claimValue = this.fetchValue(userId, tenantId); + function build(userId, recipeUserId, tenantId) { + claimValue = this.fetchValue(userId, recipeUserId, tenantId); return this.addToPayload_internal({}, claimValue) } } diff --git a/v2/session/common-customizations/sessions/fetch-sessions-for-user.mdx b/v2/session/common-customizations/sessions/fetch-sessions-for-user.mdx index 11123c2dc..bf45db6c2 100644 --- a/v2/session/common-customizations/sessions/fetch-sessions-for-user.mdx +++ b/v2/session/common-customizations/sessions/fetch-sessions-for-user.mdx @@ -122,6 +122,6 @@ for session_handle in session_handles: -By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to delete the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. +By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to fetch the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. \ No newline at end of file diff --git a/v2/session/common-customizations/sessions/jwt-signing-key-rotation.mdx b/v2/session/common-customizations/sessions/jwt-signing-key-rotation.mdx index 5ee8a1606..273c8e6ca 100644 --- a/v2/session/common-customizations/sessions/jwt-signing-key-rotation.mdx +++ b/v2/session/common-customizations/sessions/jwt-signing-key-rotation.mdx @@ -90,6 +90,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -114,6 +119,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -134,9 +152,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -147,7 +162,10 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + \ No newline at end of file diff --git a/v2/session/common-customizations/sessions/revoke-session.mdx b/v2/session/common-customizations/sessions/revoke-session.mdx index 1dfc9b63a..7eee36246 100644 --- a/v2/session/common-customizations/sessions/revoke-session.mdx +++ b/v2/session/common-customizations/sessions/revoke-session.mdx @@ -46,7 +46,7 @@ function NavBar() { async function onLogout() { // highlight-next-line await signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } return (
            @@ -71,7 +71,7 @@ import Session from "supertokens-auth-react/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -101,7 +101,7 @@ import Session from "supertokens-web-js/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -113,7 +113,7 @@ import supertokensSession from "supertokens-web-js-script/recipe/session"; async function logout () { // highlight-next-line await supertokensSession.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` diff --git a/v2/session/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/session/common-customizations/sessions/with-jwt/jwt-verification.mdx index 98888e553..e3916b7a5 100644 --- a/v2/session/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/session/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -170,6 +170,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -194,6 +199,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -214,9 +232,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -227,9 +242,12 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + diff --git a/v2/src/components/appInfoForm/index.tsx b/v2/src/components/appInfoForm/index.tsx index c75878ddd..2f0f58c41 100644 --- a/v2/src/components/appInfoForm/index.tsx +++ b/v2/src/components/appInfoForm/index.tsx @@ -341,7 +341,7 @@ export default class AppInfoForm extends React.PureComponent ( - You can view the login UI by visiting {this.state.websiteBasePath || "/"}. + You can view the login UI by visiting {this.state.websiteBasePath || "/"}. You can also see all designs of our pre built UI, for each page on this link. ) diff --git a/v2/src/plugins/codeTypeChecking/jsEnv/package.json b/v2/src/plugins/codeTypeChecking/jsEnv/package.json index 8fb27cc14..6f4760c7d 100644 --- a/v2/src/plugins/codeTypeChecking/jsEnv/package.json +++ b/v2/src/plugins/codeTypeChecking/jsEnv/package.json @@ -55,14 +55,14 @@ "react-router-dom5": "npm:react-router-dom@^5.3.0", "socket.io": "^4.6.1", "socketio": "^1.0.0", - "supertokens-auth-react": "^0.37.0", - "supertokens-node": "^16.7.0", + "supertokens-auth-react": "github:supertokens/supertokens-auth-react#0.39", + "supertokens-node": "github:supertokens/supertokens-node#17.0", "supertokens-node7": "npm:supertokens-node@7.3", "supertokens-react-native": "^4.0.0", - "supertokens-web-js": "^0.9.0", - "supertokens-web-js-script": "github:supertokens/supertokens-web-js#0.9", - "supertokens-website": "^18.0.0", - "supertokens-website-script": "github:supertokens/supertokens-website#18.0", + "supertokens-web-js": "github:supertokens/supertokens-web-js#0.10", + "supertokens-web-js-script": "github:supertokens/supertokens-web-js#0.10", + "supertokens-website": "^17.0.0", + "supertokens-website-script": "github:supertokens/supertokens-website#17.0", "typescript": "^4.9.5" } -} +} \ No newline at end of file diff --git a/v2/src/plugins/markdownVariables.json b/v2/src/plugins/markdownVariables.json index 13dc5220f..74609122a 100644 --- a/v2/src/plugins/markdownVariables.json +++ b/v2/src/plugins/markdownVariables.json @@ -84,6 +84,7 @@ "goSignIn": "EmailPasswordSignIn", "pythonSignIn": "emailpassword_sign_in", "nodeSignIn": "emailPasswordSignIn", + "nodeVerifyCredentials": "emailPasswordVerifyCredentials", "nodeRecipeInitDefault": "// typecheck-only, removed from output", "goRecipeInitDefault": "", "pythonDefaultCallbackDefs": "", @@ -231,6 +232,7 @@ "goSignIn": "SignIn", "pythonSignIn": "sign_in", "nodeSignIn": "signIn", + "nodeVerifyCredentials": "verifyCredentials", "nodeRecipeInitDefault": "", "goRecipeInitDefault": "", "pythonDefaultCallbackDefs": "", diff --git a/v2/thirdparty/advanced-customizations/frontend-hooks/handle-event.mdx b/v2/thirdparty/advanced-customizations/frontend-hooks/handle-event.mdx index 9aec0a58b..45ba3aab1 100644 --- a/v2/thirdparty/advanced-customizations/frontend-hooks/handle-event.mdx +++ b/v2/thirdparty/advanced-customizations/frontend-hooks/handle-event.mdx @@ -23,11 +23,15 @@ ThirdParty.init({ // called when a user visits the login / sign up page with a valid session // in this case, they are usually redirected to the main app } else if (context.action === "SUCCESS") { - let user = context.user; - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // sign up success + if (context.createdNewSession) { + let user = context.user; + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // sign up success + } else { + // sign in success + } } else { - // sign in success + // this is during linking a social account to an existing account } } } diff --git a/v2/thirdparty/advanced-customizations/frontend-hooks/redirection-callback.mdx b/v2/thirdparty/advanced-customizations/frontend-hooks/redirection-callback.mdx index a96515fdf..09398a627 100644 --- a/v2/thirdparty/advanced-customizations/frontend-hooks/redirection-callback.mdx +++ b/v2/thirdparty/advanced-customizations/frontend-hooks/redirection-callback.mdx @@ -16,17 +16,23 @@ This function is used to change where the user is redirected to post certain act ```tsx import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; +import SuperTokens from "supertokens-auth-react" -ThirdParty.init({ +SuperTokens.init({ + appInfo: { + appName: "SuperTokens", + apiDomain: "http://localhost:3000", + websiteDomain: "http://localhost:3000", + }, getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { + if (context.action === "SUCCESS" && context.newSessionCreated) { // called on a successful sign in / up. Where should the user go next? let redirectToPath = context.redirectToPath; if (redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return redirectToPath; } - if (context.isNewPrimaryUser) { + if (context.createdNewUser) { // user signed up return "/onboarding" } else { @@ -36,8 +42,11 @@ ThirdParty.init({ } // return undefined to let the default behaviour play out return undefined; - } -}); + }, + recipeList: [ + ThirdParty.init(/* ... */) + ] +}) ``` diff --git a/v2/thirdparty/advanced-customizations/user-context.mdx b/v2/thirdparty/advanced-customizations/user-context.mdx index a9f68e800..a8e820728 100644 --- a/v2/thirdparty/advanced-customizations/user-context.mdx +++ b/v2/thirdparty/advanced-customizations/user-context.mdx @@ -57,7 +57,7 @@ SuperTokens.init({ signInUp: async function (input) { let resp = await originalImplementation.signInUp(input); if (resp.status === "OK" && resp.createdNewRecipeUser && - resp.user.loginMethods.length === 1) { + resp.user.loginMethods.length === 1 && input.session === undefined) { /* * This is called during the signInUp API for third party login, * but before calling the createNewSession function. diff --git a/v2/thirdparty/common-customizations/account-linking/adding-accounts-to-session.mdx b/v2/thirdparty/common-customizations/account-linking/adding-accounts-to-session.mdx new file mode 100644 index 000000000..b8b928c10 --- /dev/null +++ b/v2/thirdparty/common-customizations/account-linking/adding-accounts-to-session.mdx @@ -0,0 +1,344 @@ +--- +id: adding-accounts-to-session +title: Linking social accounts or adding a password to an existing account +hide_title: true +--- + +import AccountLinkingPaidBanner from '../../../community/reusableMD/accountlinking/AccountLinkingPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Linking social accounts or adding a password to an existing account + +There may be scenarios in which you want to link a social account to an existing user account, or add a password to an account that was created using a social provider (or passwordless login). This guide will walk you through how to do this. + +The idea here is that we reuse the existing sign up APIs, but call them with a session's access token. The APIs will then create a new recipe user for that login method based on the input, and then link that to the session user. Of course, there are security checks done to ensure there is no account takeover risk, and we will go through them in this guide as well. + +:::caution +We do not provide pre built UIs for this flow since it's probably something you want to add in your settings page or during the sign up process, so this guide will focus on which APIs to call from your own UI. + +The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised our `supertokens-auth-react` SDK, on the frontend. +::: + +## Linking a social account to an existing user account + +### Step 1: Enable account linking on the backend SDK + + + + +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +supertokens.init({ + supertokens: { + connectionURI: "...", + apiKey: "..." + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (user === undefined) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + if (session !== undefined && session.getUserId() === user.id) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + return { + shouldAutomaticallyLink: false + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +In the above implementation of `shouldDoAutomaticAccountLinking`, we only allow account linking if the input session is present. This means that we are trying to link a social login account to an existing session user. Otherwise, we do not allow account linking which means that first factor account linking is disabled. If you want to enable that too, you can see [this page](./automatic-account-linking). + +### Step 2: Create a UI to show social login buttons and handle login + +First, you will need to detect which social login methods are already linked to the user. This can be done by inspecting the [user object](../../user-object) on the backend and checking the `thirdParty.id` property (the values will be like `google`, `facebook` etc). + +Then you will have to create your own UI which asks the user to pick a social login provider to connect to. Once they click on one, you will redirect them to that provider's page. Post login, the provider will redirect the user back to your application (on the same path as the first factor login) after which you will call our APIs to consume the OAuth tokens and link the user. + +The exact implementation of the above can be found [here](../../custom-ui/thirdparty-login). The two big differences in the implementation are: +- When you call the signinup API, you need to provide the session's access token in the request. If you are using our frontend SDK, this is done automatically via our frontend network interceptors. The access token will enable the backend to get a session and then link the social login account to session user. +- There are new types of failure scenarios when calling the signinup API which are not possible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#err_code_001) (> `ERR_CODE_008`). + +### Step 3: Extract the social login access token and user peofile info on the backend + +Once you call the signinup API from the frontend, SuperTokens will verify the OAuth tokens and fetch the user's profile info from the third party provider. SuperTokens will also link the newly created recipe user to the session user. + +To fetch the new user object and also the third party profile, you can override the signinup recipe function: + + + + +```tsx +import SuperTokens, { User } from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // override the thirdparty sign in / up function + signInUp: async function (input) { + + let existingUser: User | undefined = undefined; + if (input.session !== undefined) { + existingUser = await SuperTokens.getUser(input.session.getUserId()); + } + + + let response = await originalImplementation.signInUp(input); + + if (response.status === "OK") { + + let accessToken = response.oAuthTokens["access_token"]; + + let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; + + if (input.session !== undefined && response.user.id === input.session.getUserId()) { + if (response.user.loginMethods.length === existingUser!.loginMethods.length + 1) { + // new social account was linked to session user + } else { + // social account was already linked to the session + // user from before + } + } + } + + return response; + } + } + } + } + // highlight-end + }), + Session.init({ /* ... */ }) + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +Notice in the above snippet that we check for `input.session !== undefined && response.user.id === input.session.getUserId()`. This ensures that we run our custom logic only if it's linking a social account to your session account, and not during first factor login. + +## Adding a password to an existing account + +### Step 1: Enable account linking and emailpassword on the backend SDK + + + + +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; +import EmailPassword from "supertokens-node/recipe/emailpassword" + +supertokens.init({ + supertokens: { + connectionURI: "...", + apiKey: "..." + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + EmailPassword.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (user === undefined) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + if (session !== undefined && session.getUserId() === user.id) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + return { + shouldAutomaticallyLink: false + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +In the above implementation of `shouldDoAutomaticAccountLinking`, we only allow account linking if the input session is present. This means that we are trying to link an email password account to an existing session user. Otherwise, we do not allow account linking which means that first factor account linking is disabled. If you want to enable that too, you can see [this page](./automatic-account-linking). + +### Step 2: Create a UI to show a password input to the user and handle the submit event + +:::important +If you want to use password based auth as a second factor, or for step up auth, see our docs in the [MFA recipe](/docs/mfa/introduction) instead. The guide below is only meant for if you want to add a password for a user and allow them to login via email password for first factor login. +::: + +First, you will need to detect if there already exists a password for the user. This can be done by inspecting the [user object](../../user-object) on the backend and checking if there is an `emailpassword` login method. + +Then, if no such login method exists, you will have to show a UI in which the user can add a password to their account. The default password validation rules can be found [here](/docs/emailpassword/common-customizations/signup-form/field-validators#changing-the-default-email-and-password-validators). + +You will also need to fetch the email of the user before you call the email password sign up API. Once again, you can fetch this using the the user object. If the user object does not have an email (which can only happen if the first factor is phone OTP), then you should ask the user to go through an email OTP flow (via out passwordless recipe) before asking them to set a password. Thge email OTP flow will also result in a passwordless user account being created and linked to the session user. + +Once you have the email on the frontend, you should call the [sign up API](/docs/emailpassword/custom-ui/email-password-login#sign-up-form). The two big differences in the implementation are: +- When you call the signup API, you need to provide the session's access token in the request. If you are using our frontend SDK, this is done automatically via our frontend network interceptors. The access token will enable the backend to get a session and then link the email password account to session user. +- There are new types of failure scenarios when calling the signup API which are not possible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#err_code_001) (> `ERR_CODE_008`). + +### Step 3: Checking for email match in the backend sign up API +Since the email is specified on the frontend, we want to verify it in the backend API before using it (since we shouldn't trust the frontend). This can be easily done by overriding the email password sign up API: + + + + +```tsx +import SuperTokens from "supertokens-node"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + // highlight-start + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signUpPOST: async function (input) { + if (input.session !== undefined) { + // this means that we are trying to add a password to the session user + const inputEmail = input.formFields.find(f => f.id === "email")!.value; + let sessionUserId = input.session.getUserId(); + let userObject = await SuperTokens.getUser(sessionUserId); + if (userObject!.emails.find(e => e === inputEmail) === undefined) { + // this means that the input email does not belong to this user. + return { + status: "GENERAL_ERROR", + message: "Cannot use this email to add a password for this user" + } + } + } + return await originalImplementation.signUpPOST!(input); + } + } + } + } + // highlight-end + }), + Session.init({ /* ... */ }) + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + \ No newline at end of file diff --git a/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx index 8e326cf2a..7967d1a3b 100644 --- a/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdparty/common-customizations/account-linking/automatic-account-linking.mdx @@ -32,11 +32,12 @@ You can enable this feature by providing the following callback implementation o import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { - connectionURI: "", - apiKey: "" + connectionURI: "...", + apiKey: "..." }, appInfo: { apiDomain: "...", @@ -46,7 +47,12 @@ supertokens.init({ recipeList: [ // highlight-start AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (session !== undefined) { + return { + shouldAutomaticallyLink: false + } + } if (newAccountInfo.recipeUserId !== undefined && user !== undefined) { let userId = newAccountInfo.recipeUserId.getAsString(); let hasInfoAssociatedWithUserId = false // TODO: add your own implementation here. @@ -89,6 +95,7 @@ Coming Soon Notice that in case of `newAccountInfo.recipeUserId !== undefined && user !== undefined` being `true`, we add some extra logic to check if the user ID has any info associated with them in your application db. This is to prevent data loss for this user ID (see the [migtation section below](#migration-of-user-data-when-accounts-are-linked)). - `user: User | undefined`: If this is not `undefined`, it means that `newAccountInfo` user is about to linked to this `user`. If this is `undefined`, it means that `newAccountInfo` user is about to become a primary user. +- `session: SessionContainerInterface | undefined`: The session object of the user that is about to be linked. This is `undefined` if it's the first factor login. If a user has completed the first factor, and calls a sign up / in API again of some login method (either for MFA or for social login linking), then the `session` object will be defined. In the above snippet, we prevent account linking if a session is defined so that account linking only happens during the first factor login. - `tenant: string`: The ID of the tenant that the user is signing in / up to. - `userContext: any`: User defined userContext. @@ -98,18 +105,6 @@ Coming Soon You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not. -:::caution -If enabling automatic account linking for **an existing user base**, then before you enable it, you need to mark existing users as primary users so that future sign ups / ins with the same email / phone, but with a different login method, get linked to the existing user (as opposed to the other way around). - -To do this, you should create a script that: -- [Loops through all users](../user-pagination) in your app (oldest first). -- For each user, fetch the user's ID and call the [create primary user](./manual-account-linking#creating-a-primary-user) function on them **if their email is verified**. - - If at any point, you find that primary user creation failed because of `ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR` error, then it means that there is already another user with the same email / phone number that is already a primary user. In this case, you don't need to take any action for the current user (that failed) since they will be auto linked to the existing user when they sign in next with this method. As a side effect of this, their primary user ID will change when their account will be linked. To do any data migration at this point, see [this section on this page](#migration-of-user-data-when-accounts-are-linked). - - If you have two users with the same email / phone number, you can even pick which one of them will become a primary user based on your own logic. For example, instead of just making the oldest user the primary user, you can choose the one that has more activity on your app. -- You can also do the above via [direct API calls to the core](https://app.swaggerhub.com/apis/supertokens/CDI). -- When looping through users, and if you are using our managed service, make sure to add artificial delay in the loop to not breach [our rate limits](../../rate-limits). In case the API call or function calls throws an error with a `429`, please wait for a few seconds and then retry the call. -- In order to be able to do this, you will first need to enable account linking feature in the SuperTokens core. For self hosted core, you can get a license key by [signing up](https://supertokens.com/auth), and for managed service users, you can enable the feature [from the dashboard](https://supertokens.com/dashboard-saas). -::: ## Different scenarios of automatic account linking @@ -165,11 +160,12 @@ To prevent this scenario, you should: import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId, RecipeLevelUser } from "supertokens-node/recipe/accountlinking/types"; + import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { - connectionURI: "", - apiKey: "" + connectionURI: "...", + apiKey: "..." }, appInfo: { apiDomain: "...", @@ -178,7 +174,7 @@ To prevent this scenario, you should: }, recipeList: [ AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { return { shouldAutomaticallyLink: true, shouldRequireVerification: true @@ -373,5 +369,106 @@ The following is a list of support status codes that the end user might see duri - To resolve this issue, you can ask the user to try the reset password flow. +### ERR_CODE_014 +- This can happen when adding a password to an existing session user: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" + } + ``` +- An example scenario of when in the following scenario: + - Let's say that the app is confgured to not have autmatic account linking during the first factor. + - A user creates an email password account with email `e1`, verifies it, and links social login account to it with email `e2`. + - The user logs out, and then creates a social login account with email `e1`. Then, they are asked to add a password to this account. Since an email password account with `e1` already exists, SuperTokens will try and link that to this new account, but fail, since the email password account with `e1` is already a primary user. + +- To resolve this, we recommend that manually link the `e1` social login account with the `e1` email password account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. + +### ERR_CODE_015 +- This can happen when adding a password to an existing session user: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" + } + ``` +- An example scenario of when in the following scenario: + - A user creates a social login account with email `e1` which becomes a primary user. + - The user logs out, and creates another social login account with email `e2`, which also becomes a primary user. + - The user is asked to add a password for the new account with an option to also specify an email with it (this is strange, but theoritically possible). They now enter the email `e1` for the email password account. + - This will cause this type of error since the linking of the new social logn and email account will fail since there already exists another primary user with the same (`e1`) email. + +- To resolve this, we recommend not allowing users to specify an email when asking them to add a password for their account. + +### ERR_CODE_016 +- This can happen when adding a password to an existing session user: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" + } + ``` +- An example scenario of when in the following scenario: + - Let's say that the app is configured to not have automatic account linking during the first factor. + - A user signs up with a social login account using Google with email `e1`, and they add another social account, with Facebook, with the same email. + - The user logs out and creates another social login account with email `e1` (say Github), and then tries and adds a password to this account with email `e1`. Here, SuperTokens will try and make the Github login a primary user, but fail, since the email `e1` is already a primary user (with Google login). + +- To resolve this, we recommend that manually link the `e1` Github social login account with the `e1` Google social login account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. + +### ERR_CODE_020 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_020)" + } + ``` +- This can happen when the thirdparty account that is trying to be linked to the session's account is not verified. It could happen when you are trying to associate a social login account to a user, but that social account's email is not verified (and if the email of that account is not the same as the current session's account's email). +- To resolve this, you can return `shouldRequireVerification` as `false` in the `shouldDoAutomaticAccountLinking` function implementation, or you can only allow users to link social login accounts that give verified accounts. + +### ERR_CODE_021 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_021)" + } + ``` +- This can happen when the thirdparty account that is trying to be linked to the session's account is already linked with another primary user. + +### ERR_CODE_022 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_022)" + } + ``` +- This can happen when the thirdparty account that is trying to be linked to the session's account has the same email as another primary user. + +### ERR_CODE_023 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_023)" + } + ``` +- In order to link the third party user with the session user, we need to make sure that the session user is a primary user. However, that can fail if there exists another primary user with the same email as the session user, and in this case, this error is returned to the frontend. + ### Changing the error message on the frontend If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). diff --git a/v2/thirdparty/common-customizations/embed-sign-in-up-form.mdx b/v2/thirdparty/common-customizations/embed-sign-in-up-form.mdx index 8b870a52e..e0f0ff6d2 100644 --- a/v2/thirdparty/common-customizations/embed-sign-in-up-form.mdx +++ b/v2/thirdparty/common-customizations/embed-sign-in-up-form.mdx @@ -55,21 +55,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start - async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -118,21 +113,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start - async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -183,21 +173,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start - async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -223,8 +208,7 @@ function AuthPage() { In the above code snippet, we: 1. Disabled the default Auth UI by setting `disableDefaultUI` to `true` inside the ThirdParty recipe config. -2. Overrode the `getRedirectionURL` function inside the ThirdParty recipe config to redirect to `/dashboard` upon successful login. -3. Overrode the `getRedirectionURL` function inside the SuperTokens config to redirect to `/auth` when login is required. +2. Override the `getRedirectionURL` function inside the ThirdParty recipe config to redirect to `/dashboard` upon successful login or to `/auth` when login is required. Feel free to customize the redirection URLs as needed. diff --git a/v2/thirdparty/common-customizations/handling-signinup-success.mdx b/v2/thirdparty/common-customizations/handling-signinup-success.mdx index deef80788..741b55184 100644 --- a/v2/thirdparty/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdparty/common-customizations/handling-signinup-success.mdx @@ -123,10 +123,12 @@ SuperTokens.init({ let providerAccessToken = response.oAuthTokens["access_token"]; let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // TODO: Post sign up logic - } else { - // TODO: Post sign in logic + if (input.session === undefined) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // TODO: Post sign up logic + } else { + // TODO: Post sign in logic + } } } return response; diff --git a/v2/thirdparty/common-customizations/multi-tenancy/common-domain-login.mdx b/v2/thirdparty/common-customizations/multi-tenancy/common-domain-login.mdx index aec347cdf..020dcc924 100644 --- a/v2/thirdparty/common-customizations/multi-tenancy/common-domain-login.mdx +++ b/v2/thirdparty/common-customizations/multi-tenancy/common-domain-login.mdx @@ -289,7 +289,7 @@ async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button @@ -312,7 +312,7 @@ async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button @@ -667,23 +667,25 @@ SuperTokens.init({ appName: "...", websiteDomain: "...", }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + let claimValue: string[] | undefined = await Session.getClaimValue({ + claim: Multitenancy.AllowedDomainsClaim + }); + if (claimValue !== undefined) { + window.location.href = "https://" + claimValue[0]; + } else { + // there was no configured allowed domain for this user. Throw an error cause of + // misconfig or redirect to a default sub domain + } + } + return undefined; + }, + // highlight-end recipeList: [ ^{recipeNameCapitalLetters}.init({ ^{reactRecipeInitDefault} - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { - let claimValue: string[] | undefined = await Session.getClaimValue({ - claim: Multitenancy.AllowedDomainsClaim - }); - if (claimValue !== undefined) { - window.location.href = "https://" + claimValue[0]; - } else { - // there was no configured allowed domain for this user. Throw an error cause of - // misconfig or redirect to a default sub domain - } - } - return undefined; - } }), ] }); diff --git a/v2/thirdparty/common-customizations/multi-tenancy/sub-domain-login.mdx b/v2/thirdparty/common-customizations/multi-tenancy/sub-domain-login.mdx index e195ae5cb..04c114fcb 100644 --- a/v2/thirdparty/common-customizations/multi-tenancy/sub-domain-login.mdx +++ b/v2/thirdparty/common-customizations/multi-tenancy/sub-domain-login.mdx @@ -163,7 +163,7 @@ async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button @@ -186,7 +186,7 @@ async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button diff --git a/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx b/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx index 8900a22f7..545e51ca3 100644 --- a/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/thirdparty/common-customizations/sessions/claims/claim-validators.mdx @@ -52,17 +52,20 @@ Without a special construct of session claim validators, the updating of the ses Before we dive deep into claim validators, let's talk about session claim objects. These are objects that conform to an interface that allows SuperTokens to automatically add session claims to the access token payload. Here is the interface: ```tsx + +import { RecipeUserId } from "supertokens-node"; + type JSONObject = any; // typecheck-only, removed from output interface SessionClaim { readonly key: string; - fetchValue(userId: string, tenantId: string, userContext: any): Promise; + fetchValue(userId: string, recipeUserId: RecipeUserId, tenantId: string, currentPayload: JSONObject | undefined, userContext: any): Promise; addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject; - removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayloadByMerge_internal(payload: JSONObject, userContext: any): JSONObject; - removeFromPayload(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayload(payload: JSONObject, userContext: any): JSONObject; getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; @@ -364,7 +367,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)) }; /* @@ -509,8 +512,8 @@ The `build` function is a helper function which all claims have that does the fo ```text class Claim { // other functions like fetchValue, getValueFromPayload etc.. - function build(userId, tenantId) { - claimValue = this.fetchValue(userId, tenantId); + function build(userId, recipeUserId, tenantId) { + claimValue = this.fetchValue(userId, recipeUserId, tenantId); return this.addToPayload_internal({}, claimValue) } } diff --git a/v2/thirdparty/common-customizations/sessions/fetch-sessions-for-user.mdx b/v2/thirdparty/common-customizations/sessions/fetch-sessions-for-user.mdx index 989d8c1cb..cfec26595 100644 --- a/v2/thirdparty/common-customizations/sessions/fetch-sessions-for-user.mdx +++ b/v2/thirdparty/common-customizations/sessions/fetch-sessions-for-user.mdx @@ -121,6 +121,6 @@ for session_handle in session_handles: -By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to delete the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. +By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to fetch the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. diff --git a/v2/thirdparty/common-customizations/sessions/jwt-signing-key-rotation.mdx b/v2/thirdparty/common-customizations/sessions/jwt-signing-key-rotation.mdx index 097aeeaa9..b4ab73477 100644 --- a/v2/thirdparty/common-customizations/sessions/jwt-signing-key-rotation.mdx +++ b/v2/thirdparty/common-customizations/sessions/jwt-signing-key-rotation.mdx @@ -90,6 +90,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -114,6 +119,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -134,9 +152,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -147,7 +162,10 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + diff --git a/v2/thirdparty/common-customizations/sessions/revoke-session.mdx b/v2/thirdparty/common-customizations/sessions/revoke-session.mdx index 3c13cdc8e..a396512bf 100644 --- a/v2/thirdparty/common-customizations/sessions/revoke-session.mdx +++ b/v2/thirdparty/common-customizations/sessions/revoke-session.mdx @@ -46,7 +46,7 @@ function NavBar() { async function onLogout() { // highlight-next-line await signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } return (
              @@ -71,7 +71,7 @@ import Session from "supertokens-auth-react/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -101,7 +101,7 @@ import Session from "supertokens-web-js/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -113,7 +113,7 @@ import supertokensSession from "supertokens-web-js-script/recipe/session"; async function logout () { // highlight-next-line await supertokensSession.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` diff --git a/v2/thirdparty/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/thirdparty/common-customizations/sessions/with-jwt/jwt-verification.mdx index c986e3b97..d1f0e905a 100644 --- a/v2/thirdparty/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/thirdparty/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -170,6 +170,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -194,6 +199,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -214,9 +232,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -227,9 +242,12 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + diff --git a/v2/thirdparty/common-customizations/userid-format.mdx b/v2/thirdparty/common-customizations/userid-format.mdx index 8c62f81b8..442b7d39b 100644 --- a/v2/thirdparty/common-customizations/userid-format.mdx +++ b/v2/thirdparty/common-customizations/userid-format.mdx @@ -56,7 +56,7 @@ SuperTokens.init({ let response = await originalImplementation.signInUp(input); if (response.status === "OK") { - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1 && input.session === undefined) { // highlight-start let externalUserId = "" await SuperTokens.createUserIdMapping({ superTokensUserId: response.user.id, externalUserId }) diff --git a/v2/thirdparty/custom-ui/sign-out.mdx b/v2/thirdparty/custom-ui/sign-out.mdx index c851395f9..af9169fa0 100644 --- a/v2/thirdparty/custom-ui/sign-out.mdx +++ b/v2/thirdparty/custom-ui/sign-out.mdx @@ -28,7 +28,7 @@ import Session from "supertokens-web-js/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` @@ -40,7 +40,7 @@ import supertokensSession from "supertokens-web-js-script/recipe/session"; async function logout () { // highlight-next-line await supertokensSession.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` diff --git a/v2/thirdparty/custom-ui/thirdparty-login.mdx b/v2/thirdparty/custom-ui/thirdparty-login.mdx index b2f409a0a..a92cb4e3a 100644 --- a/v2/thirdparty/custom-ui/thirdparty-login.mdx +++ b/v2/thirdparty/custom-ui/thirdparty-login.mdx @@ -135,7 +135,10 @@ Once the third party provider redirects your user back to your app, you need to } window.location.assign("/home"); } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. Please see our account linking docs + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -175,7 +178,10 @@ Once the third party provider redirects your user back to your app, you need to } window.location.assign("/home"); } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. Please see our account linking docs + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -627,7 +633,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -835,7 +841,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -915,7 +921,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -966,7 +972,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. diff --git a/v2/thirdparty/mfa.mdx b/v2/thirdparty/mfa.mdx new file mode 100644 index 000000000..1c72b293a --- /dev/null +++ b/v2/thirdparty/mfa.mdx @@ -0,0 +1,9 @@ +--- +id: mfa +title: Multi factor auth +hide_title: true +--- + +# Multi factor auth + +See our guide for Multi Factor Auth [here](/docs/mfa/introduction). \ No newline at end of file diff --git a/v2/thirdparty/migration/about.mdx b/v2/thirdparty/migration/about.mdx index ed9b1b976..08b0ef756 100644 --- a/v2/thirdparty/migration/about.mdx +++ b/v2/thirdparty/migration/about.mdx @@ -45,3 +45,6 @@ There are 3 steps to user migration: - This will prevent users from having to re-authenticate. You can learn more about how to implement these changes in the [Session Migration](./session-migration) section. + +## Step 4) MFA migration +If you are using MFA in your app, checkout the MFA migration section [here](/docs/mfa/migration) after you have gone through the above migration steps. diff --git a/v2/thirdparty/migration/mfa-migration.mdx b/v2/thirdparty/migration/mfa-migration.mdx new file mode 100644 index 000000000..85f7c646b --- /dev/null +++ b/v2/thirdparty/migration/mfa-migration.mdx @@ -0,0 +1,12 @@ +--- +id: mfa-migration +title: Step 4) MFA migration +hide_title: true +--- + + + + +# MFA migration + +If you are using MFA in your app, checkout the MFA migration section [here](/docs/mfa/migration) after you have gone through the previous steps in migration. diff --git a/v2/thirdparty/multi-tenant.mdx b/v2/thirdparty/multi-tenant.mdx new file mode 100644 index 000000000..42d32e856 --- /dev/null +++ b/v2/thirdparty/multi-tenant.mdx @@ -0,0 +1,9 @@ +--- +id: multi-tenant +title: Multi tenancy / B2B orgs +hide_title: true +--- + +# Multi tenancy / B2B orgs + +See our guide for multi tenancy auth [here](/docs/multitenancy/introduction). \ No newline at end of file diff --git a/v2/thirdparty/nextjs/app-directory/setting-up-frontend.mdx b/v2/thirdparty/nextjs/app-directory/setting-up-frontend.mdx index 31028dab7..68c551eac 100644 --- a/v2/thirdparty/nextjs/app-directory/setting-up-frontend.mdx +++ b/v2/thirdparty/nextjs/app-directory/setting-up-frontend.mdx @@ -62,7 +62,9 @@ export default function Auth() { ## 3) Visit `/auth` page on your website -If you see a login UI, then you have successfully completed this step! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) +If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre built UI, for each page on [this link](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/auth-page--playground). + +If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/v2/thirdparty/nextjs/setting-up-frontend.mdx b/v2/thirdparty/nextjs/setting-up-frontend.mdx index 9f7cf1295..e1f0a9e94 100644 --- a/v2/thirdparty/nextjs/setting-up-frontend.mdx +++ b/v2/thirdparty/nextjs/setting-up-frontend.mdx @@ -59,7 +59,9 @@ export default function Auth() { ## 3) Visit `/auth` page on your website -If you see a login UI, then you have successfully completed this step! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) +If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre built UI, for each page on [this link](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/auth-page--playground). + +If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/v2/thirdparty/pre-built-ui/auth-redirection.mdx b/v2/thirdparty/pre-built-ui/auth-redirection.mdx index c5ca52af0..7495a1784 100644 --- a/v2/thirdparty/pre-built-ui/auth-redirection.mdx +++ b/v2/thirdparty/pre-built-ui/auth-redirection.mdx @@ -30,26 +30,26 @@ SuperTokens.init({ appName: "...", websiteDomain: "...", }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + if (context.redirectToPath !== undefined) { + // we are navigating back to where the user was before they authenticated + return context.redirectToPath; + } + if (context.createdNewUser) { + // user signed up + } else { + // user signed in + } + return "/dashboard"; + } + return undefined; + }, + // highlight-end recipeList: [ ^{recipeNameCapitalLetters}.init({ ^{reactRecipeInitDefault} // typecheck-only, removed from output - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { - if (context.redirectToPath !== undefined) { - // we are navigating back to where the user was before they authenticated - return context.redirectToPath; - } - if (context.isNewPrimaryUser) { - // user signed up - } else { - // user signed in - } - return "/dashboard"; - } - return undefined; - } - // highlight-end }), ] }); diff --git a/v2/thirdparty/pre-built-ui/sign-out.mdx b/v2/thirdparty/pre-built-ui/sign-out.mdx index 6cf670151..ed1a43dca 100644 --- a/v2/thirdparty/pre-built-ui/sign-out.mdx +++ b/v2/thirdparty/pre-built-ui/sign-out.mdx @@ -26,7 +26,7 @@ function NavBar() { async function onLogout() { // highlight-next-line await signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } return (
                @@ -51,7 +51,7 @@ import Session from "supertokens-auth-react/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` diff --git a/v2/thirdparty/sidebars.js b/v2/thirdparty/sidebars.js index 77e1cc5da..c302764d2 100644 --- a/v2/thirdparty/sidebars.js +++ b/v2/thirdparty/sidebars.js @@ -478,7 +478,8 @@ module.exports = { "common-customizations/account-linking/overview", "common-customizations/account-linking/automatic-account-linking", "common-customizations/account-linking/manual-account-linking", - "common-customizations/account-linking/security-considerations" + "common-customizations/account-linking/security-considerations", + "common-customizations/account-linking/adding-accounts-to-session" ] }, { @@ -669,7 +670,9 @@ module.exports = { "user-roles/get-all-roles", "user-roles/delete-roles", ], - } + }, + "mfa", + "multi-tenant" ] }, "rate-limits", @@ -697,7 +700,8 @@ module.exports = { ], }, "migration/data-migration", - "migration/session-migration" + "migration/session-migration", + "migration/mfa-migration" ], }, { diff --git a/v2/thirdparty/supabase-intergration/backend-signup-override.mdx b/v2/thirdparty/supabase-intergration/backend-signup-override.mdx index 40a559cf3..4e32021c7 100644 --- a/v2/thirdparty/supabase-intergration/backend-signup-override.mdx +++ b/v2/thirdparty/supabase-intergration/backend-signup-override.mdx @@ -54,7 +54,7 @@ let backendConfig = (): TypeInput => { // check that there is no issue with sign up and that a new user is created if (response.status === "OK" && response.createdNewRecipeUser && - response.user.loginMethods.length === 1) { + response.user.loginMethods.length === 1 && input.session === undefined) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); diff --git a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx index 30ef21aff..b45f8dd40 100644 --- a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx +++ b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/handle-event.mdx @@ -27,11 +27,19 @@ ThirdPartyEmailPassword.init({ // called when a user visits the login / sign up page with a valid session // in this case, they are usually redirected to the main app } else if (context.action === "SUCCESS") { - let user = context.user; - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // sign up success + if (context.createdNewSession) { + let user = context.user; + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // sign up success + } else { + // sign in success + } } else { - // sign in success + if (context.rid === "emailpassword") { + // during step up or second factor auth with email password + } else { + // during linking a social account to an existing account + } } } } diff --git a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx index 63be9b111..e14789bb3 100644 --- a/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx +++ b/v2/thirdpartyemailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx @@ -16,29 +16,46 @@ This function is used to change where the user is redirected to post certain act ```tsx import ThirdPartyEmailPassword from "supertokens-auth-react/recipe/thirdpartyemailpassword"; +import SuperTokens from "supertokens-auth-react"; -ThirdPartyEmailPassword.init({ +SuperTokens.init({ + appInfo: { + appName: "SuperTokens", + apiDomain: "http://localhost:3000", + websiteDomain: "http://localhost:3000" + }, getRedirectionURL: async (context) => { - if (context.action === "RESET_PASSWORD") { - // called when the user clicked on the forgot password button - } else if (context.action === "SUCCESS") { + if (context.action === "SUCCESS" && context.newSessionCreated) { // called on a successful sign in / up. Where should the user go next? let redirectToPath = context.redirectToPath; if (redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return redirectToPath; } - if (context.isNewPrimaryUser) { + if (context.createdNewUser) { // user signed up return "/onboarding" } else { // user signed in return "/dashboard" } + } else if (context.action === "TO_AUTH") { + // called when the user is not authenticated and needs to be redirected to the auth page. + return "/auth"; } // return undefined to let the default behaviour play out return undefined; - } + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + getRedirectionURL: async (context) => { + if (context.action === "RESET_PASSWORD") { + // called when the user clicked on the forgot password button + } + // return undefined to let the default behaviour play out + return undefined; + } + })] }); ``` diff --git a/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx b/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx index f8647fa0e..df047891b 100644 --- a/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx +++ b/v2/thirdpartyemailpassword/advanced-customizations/user-context.mdx @@ -53,7 +53,7 @@ SuperTokens.init({ ...originalImplementation, emailPasswordSignUp: async function (input) { let resp = await originalImplementation.emailPasswordSignUp(input); - if (resp.status === "OK" && resp.user.loginMethods.length === 1) { + if (resp.status === "OK" && resp.user.loginMethods.length === 1 && input.session === undefined) { /* * This is called during the sign up API for email password login, * but before calling the createNewSession function. @@ -66,7 +66,7 @@ SuperTokens.init({ }, thirdPartySignInUp: async function (input) { let resp = await originalImplementation.thirdPartySignInUp(input); - if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1) { + if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1 && input.session === undefined) { /* * This is called during the signInUp API for third party login, * but before calling the createNewSession function. diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/adding-accounts-to-session.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/adding-accounts-to-session.mdx new file mode 100644 index 000000000..1f3f029eb --- /dev/null +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/adding-accounts-to-session.mdx @@ -0,0 +1,341 @@ +--- +id: adding-accounts-to-session +title: Linking social accounts or adding a password to an existing account +hide_title: true +--- + +import AccountLinkingPaidBanner from '../../../community/reusableMD/accountlinking/AccountLinkingPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Linking social accounts or adding a password to an existing account + +There may be scenarios in which you want to link a social account to an existing user account, or add a password to an account that was created using a social provider (or passwordless login). This guide will walk you through how to do this. + +The idea here is that we reuse the existing sign up APIs, but call them with a session's access token. The APIs will then create a new recipe user for that login method based on the input, and then link that to the session user. Of course, there are security checks done to ensure there is no account takeover risk, and we will go through them in this guide as well. + +:::caution +We do not provide pre built UIs for this flow since it's probably something you want to add in your settings page or during the sign up process, so this guide will focus on which APIs to call from your own UI. + +The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised our `supertokens-auth-react` SDK, on the frontend. +::: + +## Linking a social account to an existing user account + +### Step 1: Enable account linking on the backend SDK + + + + +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +supertokens.init({ + supertokens: { + connectionURI: "...", + apiKey: "..." + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (user === undefined) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + if (session !== undefined && session.getUserId() === user.id) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + return { + shouldAutomaticallyLink: false + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +In the above implementation of `shouldDoAutomaticAccountLinking`, we only allow account linking if the input session is present. This means that we are trying to link a social login account to an existing session user. Otherwise, we do not allow account linking which means that first factor account linking is disabled. If you want to enable that too, you can see [this page](./automatic-account-linking). + +### Step 2: Create a UI to show social login buttons and handle login + +First, you will need to detect which social login methods are already linked to the user. This can be done by inspecting the [user object](../../user-object) on the backend and checking the `thirdParty.id` property (the values will be like `google`, `facebook` etc). + +Then you will have to create your own UI which asks the user to pick a social login provider to connect to. Once they click on one, you will redirect them to that provider's page. Post login, the provider will redirect the user back to your application (on the same path as the first factor login) after which you will call our APIs to consume the OAuth tokens and link the user. + +The exact implementation of the above can be found [here](../../custom-ui/thirdparty-login). The two big differences in the implementation are: +- When you call the signinup API, you need to provide the session's access token in the request. If you are using our frontend SDK, this is done automatically via our frontend network interceptors. The access token will enable the backend to get a session and then link the social login account to session user. +- There are new types of failure scenarios when calling the signinup API which are not possible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#err_code_001) (> `ERR_CODE_008`). + +### Step 3: Extract the social login access token and user peofile info on the backend + +Once you call the signinup API from the frontend, SuperTokens will verify the OAuth tokens and fetch the user's profile info from the third party provider. SuperTokens will also link the newly created recipe user to the session user. + +To fetch the new user object and also the third party profile, you can override the signinup recipe function: + + + + +```tsx +import SuperTokens, { User } from "supertokens-node"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // override the thirdparty sign in / up function + thirdPartySignInUp: async function (input) { + + let existingUser: User | undefined = undefined; + if (input.session !== undefined) { + existingUser = await SuperTokens.getUser(input.session.getUserId()); + } + + let response = await originalImplementation.thirdPartySignInUp(input); + + if (response.status === "OK") { + + let accessToken = response.oAuthTokens["access_token"]; + + let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; + + if (input.session !== undefined && response.user.id === input.session.getUserId()) { + if (response.user.loginMethods.length === existingUser!.loginMethods.length + 1) { + // new social account was linked to session user + } else { + // social account was already linked to the session + // user from before + } + } + } + + return response; + } + } + } + } + // highlight-end + }), + Session.init({ /* ... */ }) + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +Notice in the above snippet that we check for `input.session !== undefined && response.user.id === input.session.getUserId()`. This ensures that we run our custom logic only if it's linking a social account to your session account, and not during first factor login. + +## Adding a password to an existing account + +### Step 1: Enable account linking on the backend SDK + + + + +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +supertokens.init({ + supertokens: { + connectionURI: "...", + apiKey: "..." + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (user === undefined) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + if (session !== undefined && session.getUserId() === user.id) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + return { + shouldAutomaticallyLink: false + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +In the above implementation of `shouldDoAutomaticAccountLinking`, we only allow account linking if the input session is present. This means that we are trying to link an email password account to an existing session user. Otherwise, we do not allow account linking which means that first factor account linking is disabled. If you want to enable that too, you can see [this page](./automatic-account-linking). + +### Step 2: Create a UI to show a password input to the user and handle the submit event + +:::important +If you want to use password based auth as a second factor, or for step up auth, see our docs in the [MFA recipe](/docs/mfa/introduction) instead. The guide below is only meant for if you want to add a password for a user and allow them to login via email password for first factor login. +::: + +First, you will need to detect if there already exists a password for the user. This can be done by inspecting the [user object](../../user-object) on the backend and checking if there is an `emailpassword` login method. + +Then, if no such login method exists, you will have to show a UI in which the user can add a password to their account. The default password validation rules can be found [here](../signup-form/field-validators#changing-the-default-email-and-password-validators). + +You will also need to fetch the email of the user before you call the email password sign up API. Once again, you can fetch this using the the user object. If the user object does not have an email (which can only happen if the first factor is phone OTP), then you should ask the user to go through an email OTP flow (via out passwordless recipe) before asking them to set a password. Thge email OTP flow will also result in a passwordless user account being created and linked to the session user. + +Once you have the email on the frontend, you should call the [sign up API](../../custom-ui/email-password-login#sign-up-form). The two big differences in the implementation are: +- When you call the signup API, you need to provide the session's access token in the request. If you are using our frontend SDK, this is done automatically via our frontend network interceptors. The access token will enable the backend to get a session and then link the email password account to session user. +- There are new types of failure scenarios when calling the signup API which are not possible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#err_code_001) (> `ERR_CODE_008`). + +### Step 3: Checking for email match in the backend sign up API +Since the email is specified on the frontend, we want to verify it in the backend API before using it (since we shouldn't trust the frontend). This can be easily done by overriding the email password sign up API: + + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + // highlight-start + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + emailPasswordSignUpPOST: async function (input) { + if (input.session !== undefined) { + // this means that we are trying to add a password to the session user + const inputEmail = input.formFields.find(f => f.id === "email")!.value; + let sessionUserId = input.session.getUserId(); + let userObject = await SuperTokens.getUser(sessionUserId); + if (userObject!.emails.find(e => e === inputEmail) === undefined) { + // this means that the input email does not belong to this user. + return { + status: "GENERAL_ERROR", + message: "Cannot use this email to add a password for this user" + } + } + } + return await originalImplementation.emailPasswordSignUpPOST!(input); + } + } + } + } + // highlight-end + }), + Session.init({ /* ... */ }) + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + \ No newline at end of file diff --git a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx index 8b88c3ba7..9867cf517 100644 --- a/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/account-linking/automatic-account-linking.mdx @@ -32,11 +32,12 @@ You can enable this feature by providing the following callback implementation o import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { - connectionURI: "", - apiKey: "" + connectionURI: "...", + apiKey: "..." }, appInfo: { apiDomain: "...", @@ -46,7 +47,12 @@ supertokens.init({ recipeList: [ // highlight-start AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (session !== undefined) { + return { + shouldAutomaticallyLink: false + } + } if (newAccountInfo.recipeUserId !== undefined && user !== undefined) { let userId = newAccountInfo.recipeUserId.getAsString(); let hasInfoAssociatedWithUserId = false // TODO: add your own implementation here. @@ -89,6 +95,7 @@ Coming Soon Notice that in case of `newAccountInfo.recipeUserId !== undefined && user !== undefined` being `true`, we add some extra logic to check if the user ID has any info associated with them in your application db. This is to prevent data loss for this user ID (see the [migtation section below](#migration-of-user-data-when-accounts-are-linked)). - `user: User | undefined`: If this is not `undefined`, it means that `newAccountInfo` user is about to linked to this `user`. If this is `undefined`, it means that `newAccountInfo` user is about to become a primary user. +- `session: SessionContainerInterface | undefined`: The session object of the user that is about to be linked. This is `undefined` if it's the first factor login. If a user has completed the first factor, and calls a sign up / in API again of some login method (either for MFA or for social login linking), then the `session` object will be defined. In the above snippet, we prevent account linking if a session is defined so that account linking only happens during the first factor login. - `tenant: string`: The ID of the tenant that the user is signing in / up to. - `userContext: any`: User defined userContext. @@ -98,18 +105,6 @@ Coming Soon You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not. -:::caution -If enabling automatic account linking for **an existing user base**, then before you enable it, you need to mark existing users as primary users so that future sign ups / ins with the same email / phone, but with a different login method, get linked to the existing user (as opposed to the other way around). - -To do this, you should create a script that: -- [Loops through all users](../user-pagination) in your app (oldest first). -- For each user, fetch the user's ID and call the [create primary user](./manual-account-linking#creating-a-primary-user) function on them **if their email is verified**. - - If at any point, you find that primary user creation failed because of `ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR` error, then it means that there is already another user with the same email / phone number that is already a primary user. In this case, you don't need to take any action for the current user (that failed) since they will be auto linked to the existing user when they sign in next with this method. As a side effect of this, their primary user ID will change when their account will be linked. To do any data migration at this point, see [this section on this page](#migration-of-user-data-when-accounts-are-linked). - - If you have two users with the same email / phone number, you can even pick which one of them will become a primary user based on your own logic. For example, instead of just making the oldest user the primary user, you can choose the one that has more activity on your app. -- You can also do the above via [direct API calls to the core](https://app.swaggerhub.com/apis/supertokens/CDI). -- When looping through users, and if you are using our managed service, make sure to add artificial delay in the loop to not breach [our rate limits](../../rate-limits). In case the API call or function calls throws an error with a `429`, please wait for a few seconds and then retry the call. -- In order to be able to do this, you will first need to enable account linking feature in the SuperTokens core. For self hosted core, you can get a license key by [signing up](https://supertokens.com/auth), and for managed service users, you can enable the feature [from the dashboard](https://supertokens.com/dashboard-saas). -::: ## Different scenarios of automatic account linking @@ -165,11 +160,12 @@ To prevent this scenario, you should: import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId, RecipeLevelUser } from "supertokens-node/recipe/accountlinking/types"; + import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { - connectionURI: "", - apiKey: "" + connectionURI: "...", + apiKey: "..." }, appInfo: { apiDomain: "...", @@ -178,7 +174,7 @@ To prevent this scenario, you should: }, recipeList: [ AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { return { shouldAutomaticallyLink: true, shouldRequireVerification: true @@ -373,5 +369,106 @@ The following is a list of support status codes that the end user might see duri - To resolve this issue, you can ask the user to try the reset password flow. +### ERR_CODE_014 +- This can happen when adding a password to an existing session user: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" + } + ``` +- An example scenario of when in the following scenario: + - Let's say that the app is confgured to not have autmatic account linking during the first factor. + - A user creates an email password account with email `e1`, verifies it, and links social login account to it with email `e2`. + - The user logs out, and then creates a social login account with email `e1`. Then, they are asked to add a password to this account. Since an email password account with `e1` already exists, SuperTokens will try and link that to this new account, but fail, since the email password account with `e1` is already a primary user. + +- To resolve this, we recommend that manually link the `e1` social login account with the `e1` email password account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. + +### ERR_CODE_015 +- This can happen when adding a password to an existing session user: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" + } + ``` +- An example scenario of when in the following scenario: + - A user creates a social login account with email `e1` which becomes a primary user. + - The user logs out, and creates another social login account with email `e2`, which also becomes a primary user. + - The user is asked to add a password for the new account with an option to also specify an email with it (this is strange, but theoritically possible). They now enter the email `e1` for the email password account. + - This will cause this type of error since the linking of the new social logn and email account will fail since there already exists another primary user with the same (`e1`) email. + +- To resolve this, we recommend not allowing users to specify an email when asking them to add a password for their account. + +### ERR_CODE_016 +- This can happen when adding a password to an existing session user: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" + } + ``` +- An example scenario of when in the following scenario: + - Let's say that the app is configured to not have automatic account linking during the first factor. + - A user signs up with a social login account using Google with email `e1`, and they add another social account, with Facebook, with the same email. + - The user logs out and creates another social login account with email `e1` (say Github), and then tries and adds a password to this account with email `e1`. Here, SuperTokens will try and make the Github login a primary user, but fail, since the email `e1` is already a primary user (with Google login). + +- To resolve this, we recommend that manually link the `e1` Github social login account with the `e1` Google social login account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. + +### ERR_CODE_020 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_020)" + } + ``` +- This can happen when the thirdparty account that is trying to be linked to the session's account is not verified. It could happen when you are trying to associate a social login account to a user, but that social account's email is not verified (and if the email of that account is not the same as the current session's account's email). +- To resolve this, you can return `shouldRequireVerification` as `false` in the `shouldDoAutomaticAccountLinking` function implementation, or you can only allow users to link social login accounts that give verified accounts. + +### ERR_CODE_021 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_021)" + } + ``` +- This can happen when the thirdparty account that is trying to be linked to the session's account is already linked with another primary user. + +### ERR_CODE_022 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_022)" + } + ``` +- This can happen when the thirdparty account that is trying to be linked to the session's account has the same email as another primary user. + +### ERR_CODE_023 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_023)" + } + ``` +- In order to link the third party user with the session user, we need to make sure that the session user is a primary user. However, that can fail if there exists another primary user with the same email as the session user, and in this case, this error is returned to the frontend. + ### Changing the error message on the frontend If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). \ No newline at end of file diff --git a/v2/thirdpartyemailpassword/common-customizations/change-password.mdx b/v2/thirdpartyemailpassword/common-customizations/change-password.mdx index 69be61625..6ea3055bc 100644 --- a/v2/thirdpartyemailpassword/common-customizations/change-password.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/change-password.mdx @@ -137,7 +137,7 @@ app.post("/change-password", verifySession(), async (req: SessionRequest, res: e const email = loginMethod.email!; // call signin to check that input password is correct - let isPasswordValid = await ^{recipeNameCapitalLetters}.^{nodeSignIn}(session!.getTenantId(), email, oldPassword) + let isPasswordValid = await ^{recipeNameCapitalLetters}.^{nodeVerifyCredentials}(session!.getTenantId(), email, oldPassword) if (isPasswordValid.status !== "OK") { // TODO: handle incorrect password error diff --git a/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx b/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx index 4dc86ee3f..293fb2174 100644 --- a/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/disable-sign-up/emailpassword-changes.mdx @@ -459,25 +459,25 @@ const FAKE_PASSWORD = "asokdA87fnf30efjoiOI**cwjkn"; export function POST(request: NextRequest) { return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - const body = await request.json(); - let email = body.email; + if (err) { + return NextResponse.json(err, { status: 500 }); + } + const body = await request.json(); + let email = body.email; - let signUpResult = await ^{recipeNameCapitalLetters}.^{webjsEmailPasswordSignUp}("public", email, FAKE_PASSWORD); - if (signUpResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return NextResponse.json({ message: 'User already exists' }, { status: 400 }); - } + let signUpResult = await ^{recipeNameCapitalLetters}.^{webjsEmailPasswordSignUp}("public", email, FAKE_PASSWORD); + if (signUpResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { + return NextResponse.json({ message: 'User already exists' }, { status: 400 }); + } - // we successfully created the user. Now we should send them their invite link - await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); + // we successfully created the user. Now we should send them their invite link + await ^{recipeNameCapitalLetters}.sendResetPasswordEmail("public", signUpResult.user.id, email); - return NextResponse.json({ message: 'Success' }) + return NextResponse.json({ message: 'Success' }) }, { - overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] - } + overrideGlobalClaimValidators: async function (globalClaimValidators) { + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] + } }); } ``` @@ -509,8 +509,8 @@ export class CreateUserController { let signUpResult = await ^{recipeNameCapitalLetters}.^{webjsEmailPasswordSignUp}("public", email, FAKE_PASSWORD); if (signUpResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { - // TODO: send 400 response to the client. - return; + // TODO: send 400 response to the client. + return; } // we successfully created the user. Now we should send them their invite link diff --git a/v2/thirdpartyemailpassword/common-customizations/embed-sign-in-up-form.mdx b/v2/thirdpartyemailpassword/common-customizations/embed-sign-in-up-form.mdx index 5f08dd844..3a6ec7082 100644 --- a/v2/thirdpartyemailpassword/common-customizations/embed-sign-in-up-form.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/embed-sign-in-up-form.mdx @@ -55,21 +55,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -118,21 +113,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -183,21 +173,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -223,8 +208,7 @@ function AuthPage() { In the above code snippet, we: 1. Disabled the default Auth UI by setting `disableDefaultUI` to `true` inside the ThirdPartyEmailPassword recipe config. -2. Overrode the `getRedirectionURL` function inside the ThirdPartyEmailPassword recipe config to redirect to `/dashboard` upon successful login. -3. Overrode the `getRedirectionURL` function inside the SuperTokens config to redirect to `/auth` when login is required. +2. Override the `getRedirectionURL` function inside the ThirdPartyEmailPassword recipe config to redirect to `/dashboard` upon successful login and to redirect to `/auth` when login is required. Feel free to customize the redirection URLs as needed. diff --git a/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx b/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx index 40f3b6240..1385967ed 100644 --- a/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/handling-signinup-success.mdx @@ -120,7 +120,7 @@ SuperTokens.init({ let response = await originalImplementation.emailPasswordSignUp(input); - if (response.status === "OK" && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { // TODO: some post sign up logic } @@ -133,7 +133,7 @@ SuperTokens.init({ let response = await originalImplementation.emailPasswordSignIn(input); - if (response.status === "OK") { + if (response.status === "OK" && input.session === undefined) { // TODO: some post sign in logic } @@ -152,10 +152,12 @@ SuperTokens.init({ let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // TODO: some post sign up logic - } else { - // TODO: some post sign in logic + if (input.session === undefined) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // TODO: some post sign up logic + } else { + // TODO: some post sign in logic + } } } @@ -414,7 +416,7 @@ SuperTokens.init({ let response = await originalImplementation.emailPasswordSignUpPOST!(input); // Post sign up response, we check if it was successful - if (response.status === "OK" && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { let { id, emails } = response.user; // TODO: sign up successful diff --git a/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/common-domain-login.mdx b/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/common-domain-login.mdx index e39d862f7..4cfcf9f40 100644 --- a/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/common-domain-login.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/common-domain-login.mdx @@ -289,7 +289,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button @@ -297,7 +297,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { // more checks for other providers } } - if (loginMethods.emailPassword.enabled) { + if (loginMethods.firstFactors.includes("emailpassword")) { // render sign in with email and password UI as well. } // more checks for other login methods... @@ -314,7 +314,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button @@ -322,7 +322,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { // more checks for other providers } } - if (loginMethods.emailPassword.enabled) { + if (loginMethods.firstFactors.includes("emailpassword")) { // render sign in with email and password UI as well. } // more checks for other login methods... @@ -671,23 +671,25 @@ SuperTokens.init({ appName: "...", websiteDomain: "...", }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + let claimValue: string[] | undefined = await Session.getClaimValue({ + claim: Multitenancy.AllowedDomainsClaim + }); + if (claimValue !== undefined) { + window.location.href = "https://" + claimValue[0]; + } else { + // there was no configured allowed domain for this user. Throw an error cause of + // misconfig or redirect to a default sub domain + } + } + return undefined; + }, + // highlight-end recipeList: [ ^{recipeNameCapitalLetters}.init({ ^{reactRecipeInitDefault} - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { - let claimValue: string[] | undefined = await Session.getClaimValue({ - claim: Multitenancy.AllowedDomainsClaim - }); - if (claimValue !== undefined) { - window.location.href = "https://" + claimValue[0]; - } else { - // there was no configured allowed domain for this user. Throw an error cause of - // misconfig or redirect to a default sub domain - } - } - return undefined; - } }), ] }); diff --git a/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx b/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx index 2120f7393..88720aaf0 100644 --- a/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/multi-tenancy/sub-domain-login.mdx @@ -166,7 +166,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button @@ -174,7 +174,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { // more checks for other providers } } - if (loginMethods.emailPassword.enabled) { + if (loginMethods.firstFactors.includes("emailpassword")) { // render sign in with email and password UI as well. } // more checks for other login methods... @@ -191,7 +191,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button @@ -199,7 +199,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { // more checks for other providers } } - if (loginMethods.emailPassword.enabled) { + if (loginMethods.firstFactors.includes("emailpassword")) { // render sign in with email and password UI as well. } // more checks for other login methods... diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx index 8900a22f7..545e51ca3 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/claims/claim-validators.mdx @@ -52,17 +52,20 @@ Without a special construct of session claim validators, the updating of the ses Before we dive deep into claim validators, let's talk about session claim objects. These are objects that conform to an interface that allows SuperTokens to automatically add session claims to the access token payload. Here is the interface: ```tsx + +import { RecipeUserId } from "supertokens-node"; + type JSONObject = any; // typecheck-only, removed from output interface SessionClaim { readonly key: string; - fetchValue(userId: string, tenantId: string, userContext: any): Promise; + fetchValue(userId: string, recipeUserId: RecipeUserId, tenantId: string, currentPayload: JSONObject | undefined, userContext: any): Promise; addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject; - removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayloadByMerge_internal(payload: JSONObject, userContext: any): JSONObject; - removeFromPayload(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayload(payload: JSONObject, userContext: any): JSONObject; getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; @@ -364,7 +367,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)) }; /* @@ -509,8 +512,8 @@ The `build` function is a helper function which all claims have that does the fo ```text class Claim { // other functions like fetchValue, getValueFromPayload etc.. - function build(userId, tenantId) { - claimValue = this.fetchValue(userId, tenantId); + function build(userId, recipeUserId, tenantId) { + claimValue = this.fetchValue(userId, recipeUserId, tenantId); return this.addToPayload_internal({}, claimValue) } } diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/fetch-sessions-for-user.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/fetch-sessions-for-user.mdx index 989d8c1cb..cfec26595 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/fetch-sessions-for-user.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/fetch-sessions-for-user.mdx @@ -121,6 +121,6 @@ for session_handle in session_handles: -By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to delete the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. +By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to fetch the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/jwt-signing-key-rotation.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/jwt-signing-key-rotation.mdx index 097aeeaa9..b4ab73477 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/jwt-signing-key-rotation.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/jwt-signing-key-rotation.mdx @@ -90,6 +90,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -114,6 +119,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -134,9 +152,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -147,7 +162,10 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/revoke-session.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/revoke-session.mdx index 3c13cdc8e..a396512bf 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/revoke-session.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/revoke-session.mdx @@ -46,7 +46,7 @@ function NavBar() { async function onLogout() { // highlight-next-line await signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } return (
                  @@ -71,7 +71,7 @@ import Session from "supertokens-auth-react/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -101,7 +101,7 @@ import Session from "supertokens-web-js/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -113,7 +113,7 @@ import supertokensSession from "supertokens-web-js-script/recipe/session"; async function logout () { // highlight-next-line await supertokensSession.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` diff --git a/v2/thirdpartyemailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/thirdpartyemailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx index c986e3b97..d1f0e905a 100644 --- a/v2/thirdpartyemailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -170,6 +170,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -194,6 +199,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -214,9 +232,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -227,9 +242,12 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + diff --git a/v2/thirdpartyemailpassword/common-customizations/userid-format.mdx b/v2/thirdpartyemailpassword/common-customizations/userid-format.mdx index 597c757c5..3fd92d943 100644 --- a/v2/thirdpartyemailpassword/common-customizations/userid-format.mdx +++ b/v2/thirdpartyemailpassword/common-customizations/userid-format.mdx @@ -55,7 +55,7 @@ SuperTokens.init({ emailPasswordSignUp: async function (input) { let response = await originalImplementation.emailPasswordSignUp(input); - if (response.status === "OK" && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { // highlight-start let externalUserId = "" await SuperTokens.createUserIdMapping({ superTokensUserId: response.user.id, externalUserId }) @@ -74,7 +74,7 @@ SuperTokens.init({ let response = await originalImplementation.thirdPartySignInUp(input); if (response.status === "OK") { - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1 && input.session === undefined) { // highlight-start let externalUserId = "" await SuperTokens.createUserIdMapping({ superTokensUserId: response.user.id, externalUserId }) diff --git a/v2/thirdpartyemailpassword/custom-ui/email-password-login.mdx b/v2/thirdpartyemailpassword/custom-ui/email-password-login.mdx index 2d70f8be3..b9cad763d 100644 --- a/v2/thirdpartyemailpassword/custom-ui/email-password-login.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/email-password-login.mdx @@ -57,8 +57,10 @@ async function signUpClicked(email: string, password: string) { } }) } else if (response.status === "SIGN_UP_NOT_ALLOWED") { - // this can happen during automatic account linking. Tell the user to use another - // login method, or go through the password reset flow. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign up was not allowed. + window.alert(response.reason) } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. @@ -106,8 +108,10 @@ async function signUpClicked(email: string, password: string) { } }) } else if (response.status === "SIGN_UP_NOT_ALLOWED") { - // this can happen during automatic account linking. Tell the user to use another - // login method, or go through the password reset flow. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in was not allowed. + window.alert(response.reason) } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. @@ -160,7 +164,7 @@ The response body from the API call has a `status` property in it: Either way, you want to show the user an error next to the input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking. Tell the user to use another login method, or go through the password reset flow. +- `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign up was not allowed. @@ -306,8 +310,10 @@ async function signInClicked(email: string, password: string) { } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect.") } else if (response.status === "SIGN_IN_NOT_ALLOWED") { - // this can happen due to automatic account linking. Tell the user that their - // input credentials is wrong (so that they do through the password reset flow) + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in was not allowed. + window.alert(response.reason) } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. @@ -352,8 +358,10 @@ async function signInClicked(email: string, password: string) { } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect.") } else if (response.status === "SIGN_IN_NOT_ALLOWED") { - // this can happen due to automatic account linking. Tell the user that their - // input credentials is wrong (so that they do through the password reset flow) + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in was not allowed. + window.alert(response.reason) } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. @@ -403,7 +411,7 @@ The response body from the API call has a `status` property in it: - `status: "WRONG_CREDENTIALS_ERROR"`: The input email and password combination is incorrect. - `status: "FIELD_ERROR"`: This indicates that the input email did not pass the backend validation - probably because it's syntactically not an email. You want to show the user an error next to the email input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking. Tell the user that their input credentials is wrong (so that they do through the password reset flow). +- `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in was not allowed. diff --git a/v2/thirdpartyemailpassword/custom-ui/sign-out.mdx b/v2/thirdpartyemailpassword/custom-ui/sign-out.mdx index e94d9063d..a3f2af43b 100644 --- a/v2/thirdpartyemailpassword/custom-ui/sign-out.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/sign-out.mdx @@ -28,7 +28,7 @@ import Session from "supertokens-web-js/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` @@ -40,7 +40,7 @@ import supertokensSession from "supertokens-web-js-script/recipe/session"; async function logout () { // highlight-next-line await supertokensSession.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` diff --git a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx index 9cbbcfa9f..a95affa93 100644 --- a/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartyemailpassword/custom-ui/thirdparty-login.mdx @@ -138,7 +138,10 @@ Once the third party provider redirects your user back to your app, you need to } window.location.assign("/home"); } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. Please see our account linking docs + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -178,7 +181,10 @@ Once the third party provider redirects your user back to your app, you need to } window.location.assign("/home"); } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. Please see our account linking docs + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -630,7 +636,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -838,7 +844,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -918,7 +924,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -969,7 +975,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. diff --git a/v2/thirdpartyemailpassword/mfa.mdx b/v2/thirdpartyemailpassword/mfa.mdx new file mode 100644 index 000000000..1c72b293a --- /dev/null +++ b/v2/thirdpartyemailpassword/mfa.mdx @@ -0,0 +1,9 @@ +--- +id: mfa +title: Multi factor auth +hide_title: true +--- + +# Multi factor auth + +See our guide for Multi Factor Auth [here](/docs/mfa/introduction). \ No newline at end of file diff --git a/v2/thirdpartyemailpassword/migration/about.mdx b/v2/thirdpartyemailpassword/migration/about.mdx index feafc7bab..08b0ef756 100644 --- a/v2/thirdpartyemailpassword/migration/about.mdx +++ b/v2/thirdpartyemailpassword/migration/about.mdx @@ -46,7 +46,5 @@ There are 3 steps to user migration: You can learn more about how to implement these changes in the [Session Migration](./session-migration) section. - - - - +## Step 4) MFA migration +If you are using MFA in your app, checkout the MFA migration section [here](/docs/mfa/migration) after you have gone through the above migration steps. diff --git a/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx b/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx index 38c039228..39533a118 100644 --- a/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx +++ b/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx @@ -208,7 +208,7 @@ ThirdPartyEmailPassword.init({ } // Call the signup function to create a new SuperTokens user. - let signUpResponse = await ThirdPartyEmailPassword.emailPasswordSignUp(input.email, input.password, input.userContext); + let signUpResponse = await ThirdPartyEmailPassword.emailPasswordSignUp(input.tenantId, input.email, input.password, undefined, input.userContext); if (signUpResponse.status !== "OK") { throw new Error("Should never come here") } @@ -227,7 +227,7 @@ ThirdPartyEmailPassword.init({ if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email - await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, input.userContext); + await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); } } @@ -516,7 +516,7 @@ ThirdPartyEmailPassword.init({ if (legacyUserData) { // create a SuperTokens account for the user with a temporary password let tempPassword = await generatePassword(); - let signupResponse = await ThirdPartyEmailPassword.emailPasswordSignUp(email, tempPassword, input.userContext); + let signupResponse = await ThirdPartyEmailPassword.emailPasswordSignUp(input.tenantId, email, tempPassword, undefined, input.userContext); if (signupResponse.status === "OK") { @@ -533,7 +533,7 @@ ThirdPartyEmailPassword.init({ if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email - await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, input.userContext); + await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); } } @@ -1042,7 +1042,7 @@ ThirdPartyEmailPassword.init({ } // Call the signup function to create a new SuperTokens user. - let signUpResponse = await ThirdPartyEmailPassword.emailPasswordSignUp(input.email, input.password, input.userContext); + let signUpResponse = await ThirdPartyEmailPassword.emailPasswordSignUp(input.tenantId, input.email, input.password, undefined, input.userContext); if (signUpResponse.status !== "OK") { throw new Error("Should never come here") } @@ -1060,7 +1060,7 @@ ThirdPartyEmailPassword.init({ if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email - await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, input.userContext); + await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); } } diff --git a/v2/thirdpartyemailpassword/migration/mfa-migration.mdx b/v2/thirdpartyemailpassword/migration/mfa-migration.mdx new file mode 100644 index 000000000..85f7c646b --- /dev/null +++ b/v2/thirdpartyemailpassword/migration/mfa-migration.mdx @@ -0,0 +1,12 @@ +--- +id: mfa-migration +title: Step 4) MFA migration +hide_title: true +--- + + + + +# MFA migration + +If you are using MFA in your app, checkout the MFA migration section [here](/docs/mfa/migration) after you have gone through the previous steps in migration. diff --git a/v2/thirdpartyemailpassword/multi-tenant.mdx b/v2/thirdpartyemailpassword/multi-tenant.mdx new file mode 100644 index 000000000..42d32e856 --- /dev/null +++ b/v2/thirdpartyemailpassword/multi-tenant.mdx @@ -0,0 +1,9 @@ +--- +id: multi-tenant +title: Multi tenancy / B2B orgs +hide_title: true +--- + +# Multi tenancy / B2B orgs + +See our guide for multi tenancy auth [here](/docs/multitenancy/introduction). \ No newline at end of file diff --git a/v2/thirdpartyemailpassword/nextjs/app-directory/setting-up-frontend.mdx b/v2/thirdpartyemailpassword/nextjs/app-directory/setting-up-frontend.mdx index 31028dab7..68c551eac 100644 --- a/v2/thirdpartyemailpassword/nextjs/app-directory/setting-up-frontend.mdx +++ b/v2/thirdpartyemailpassword/nextjs/app-directory/setting-up-frontend.mdx @@ -62,7 +62,9 @@ export default function Auth() { ## 3) Visit `/auth` page on your website -If you see a login UI, then you have successfully completed this step! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) +If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre built UI, for each page on [this link](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/auth-page--playground). + +If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/v2/thirdpartyemailpassword/nextjs/setting-up-frontend.mdx b/v2/thirdpartyemailpassword/nextjs/setting-up-frontend.mdx index 6c0ab1129..7151e8ee3 100644 --- a/v2/thirdpartyemailpassword/nextjs/setting-up-frontend.mdx +++ b/v2/thirdpartyemailpassword/nextjs/setting-up-frontend.mdx @@ -59,7 +59,9 @@ export default function Auth() { ## 3) Visit `/auth` page on your website -If you see a login UI, then you have successfully completed this step! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) +If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre built UI, for each page on [this link](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/auth-page--playground). + +If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/v2/thirdpartyemailpassword/pre-built-ui/auth-redirection.mdx b/v2/thirdpartyemailpassword/pre-built-ui/auth-redirection.mdx index 72a286829..9cc746323 100644 --- a/v2/thirdpartyemailpassword/pre-built-ui/auth-redirection.mdx +++ b/v2/thirdpartyemailpassword/pre-built-ui/auth-redirection.mdx @@ -30,26 +30,26 @@ SuperTokens.init({ appName: "...", websiteDomain: "...", }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + if (context.redirectToPath !== undefined) { + // we are navigating back to where the user was before they authenticated + return context.redirectToPath; + } + if (context.createdNewUser) { + // user signed up + } else { + // user signed in + } + return "/dashboard"; + } + return undefined; + }, + // highlight-end recipeList: [ ^{recipeNameCapitalLetters}.init({ ^{reactRecipeInitDefault} // typecheck-only, removed from output - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { - if (context.redirectToPath !== undefined) { - // we are navigating back to where the user was before they authenticated - return context.redirectToPath; - } - if (context.isNewPrimaryUser) { - // user signed up - } else { - // user signed in - } - return "/dashboard"; - } - return undefined; - } - // highlight-end }), ] }); diff --git a/v2/thirdpartyemailpassword/pre-built-ui/sign-out.mdx b/v2/thirdpartyemailpassword/pre-built-ui/sign-out.mdx index 2619723ed..50563ea9d 100644 --- a/v2/thirdpartyemailpassword/pre-built-ui/sign-out.mdx +++ b/v2/thirdpartyemailpassword/pre-built-ui/sign-out.mdx @@ -26,7 +26,7 @@ function NavBar() { async function onLogout() { // highlight-next-line await signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } return (
                    @@ -51,7 +51,7 @@ import Session from "supertokens-auth-react/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` diff --git a/v2/thirdpartyemailpassword/sidebars.js b/v2/thirdpartyemailpassword/sidebars.js index ba9909695..be470a6c1 100644 --- a/v2/thirdpartyemailpassword/sidebars.js +++ b/v2/thirdpartyemailpassword/sidebars.js @@ -501,7 +501,8 @@ module.exports = { "common-customizations/account-linking/overview", "common-customizations/account-linking/automatic-account-linking", "common-customizations/account-linking/manual-account-linking", - "common-customizations/account-linking/security-considerations" + "common-customizations/account-linking/security-considerations", + "common-customizations/account-linking/adding-accounts-to-session" ] }, "common-customizations/change-password", @@ -717,6 +718,8 @@ module.exports = { "user-roles/delete-roles", ], }, + "mfa", + "multi-tenant" ] }, "rate-limits", @@ -745,7 +748,8 @@ module.exports = { ], }, "migration/data-migration", - "migration/session-migration" + "migration/session-migration", + "migration/mfa-migration" ], }, { diff --git a/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx b/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx index 4f69b308d..96bd42468 100644 --- a/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx +++ b/v2/thirdpartyemailpassword/supabase-intergration/backend-signup-override.mdx @@ -52,7 +52,7 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.emailPasswordSignUpPOST(input); - if (response.status === "OK" && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); @@ -139,7 +139,7 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.thirdPartySignInUpPOST(input); // check that there is no issue with sign up and that a new user is created - if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1 && input.session === undefined) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); diff --git a/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/handle-event.mdx b/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/handle-event.mdx index 047db2105..ace8849e8 100644 --- a/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/handle-event.mdx +++ b/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/handle-event.mdx @@ -27,11 +27,19 @@ ThirdPartyPasswordless.init({ } else if (context.action === "PASSWORDLESS_CODE_SENT") { // TODO: } else if (context.action === "SUCCESS") { - const user = context.user; - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // TODO: Sign up + if (context.createdNewSession) { + let user = context.user; + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // sign up success + } else { + // sign in success + } } else { - // TODO: Sign in + if (context.rid === "passwordless") { + // during second factor authentication + } else { + // during linking a social account to an existing account + } } } }, diff --git a/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/redirection-callback.mdx b/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/redirection-callback.mdx index 892c6e27d..620481cbc 100644 --- a/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/redirection-callback.mdx +++ b/v2/thirdpartypasswordless/advanced-customizations/frontend-hooks/redirection-callback.mdx @@ -16,18 +16,23 @@ This function is used to change where the user is redirected to post certain act ```tsx import ThirdPartyPasswordless from "supertokens-auth-react/recipe/thirdpartypasswordless"; +import SuperTokens from "supertokens-auth-react" -ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", // This example will work with any contactMethod. +SuperTokens.init({ + appInfo: { + appName: "SuperTokens", + apiDomain: "http://localhost:3000", + websiteDomain: "http://localhost:3000" + }, getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { + if (context.action === "SUCCESS" && context.newSessionCreated) { // called on a successful sign in / up. Where should the user go next? let redirectToPath = context.redirectToPath; if (redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return redirectToPath; } - if (context.isNewPrimaryUser) { + if (context.createdNewUser) { // user signed up return "/onboarding" } else { @@ -37,7 +42,12 @@ ThirdPartyPasswordless.init({ } // return undefined to let the default behaviour play out return undefined; - } + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: "EMAIL", // This example will work with any contactMethod. + }) + ] }); ``` diff --git a/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx b/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx index 8eab303a8..7dcbc3567 100644 --- a/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx +++ b/v2/thirdpartypasswordless/advanced-customizations/user-context.mdx @@ -55,7 +55,7 @@ SuperTokens.init({ ...originalImplementation, consumeCode: async function (input) { let resp = await originalImplementation.consumeCode(input); - if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1) { + if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1 && input.session === undefined) { /* * This is called during the consume code API, * but before calling the createNewSession function. @@ -79,7 +79,7 @@ SuperTokens.init({ thirdPartySignInUp: async function (input) { let resp = await originalImplementation.thirdPartySignInUp(input); - if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1) { + if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1 && input.session === undefined) { /* * This is called during the signInUp API for third party login, * but before calling the createNewSession function. diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/adding-accounts-to-session.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/adding-accounts-to-session.mdx new file mode 100644 index 000000000..298937098 --- /dev/null +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/adding-accounts-to-session.mdx @@ -0,0 +1,345 @@ +--- +id: adding-accounts-to-session +title: Linking social accounts or adding a password to an existing account +hide_title: true +--- + +import AccountLinkingPaidBanner from '../../../community/reusableMD/accountlinking/AccountLinkingPaidBanner.mdx' +import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs"; + + + +# Linking social accounts or adding a password to an existing account + +There may be scenarios in which you want to link a social account to an existing user account, or add a password to an account that was created using a social provider (or passwordless login). This guide will walk you through how to do this. + +The idea here is that we reuse the existing sign up APIs, but call them with a session's access token. The APIs will then create a new recipe user for that login method based on the input, and then link that to the session user. Of course, there are security checks done to ensure there is no account takeover risk, and we will go through them in this guide as well. + +:::caution +We do not provide pre built UIs for this flow since it's probably something you want to add in your settings page or during the sign up process, so this guide will focus on which APIs to call from your own UI. + +The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised our `supertokens-auth-react` SDK, on the frontend. +::: + +## Linking a social account to an existing user account + +### Step 1: Enable account linking on the backend SDK + + + + +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +supertokens.init({ + supertokens: { + connectionURI: "...", + apiKey: "..." + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (user === undefined) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + if (session !== undefined && session.getUserId() === user.id) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + return { + shouldAutomaticallyLink: false + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +In the above implementation of `shouldDoAutomaticAccountLinking`, we only allow account linking if the input session is present. This means that we are trying to link a social login account to an existing session user. Otherwise, we do not allow account linking which means that first factor account linking is disabled. If you want to enable that too, you can see [this page](./automatic-account-linking). + +### Step 2: Create a UI to show social login buttons and handle login + +First, you will need to detect which social login methods are already linked to the user. This can be done by inspecting the [user object](../../user-object) on the backend and checking the `thirdParty.id` property (the values will be like `google`, `facebook` etc). + +Then you will have to create your own UI which asks the user to pick a social login provider to connect to. Once they click on one, you will redirect them to that provider's page. Post login, the provider will redirect the user back to your application (on the same path as the first factor login) after which you will call our APIs to consume the OAuth tokens and link the user. + +The exact implementation of the above can be found [here](../../custom-ui/thirdparty-login). The two big differences in the implementation are: +- When you call the signinup API, you need to provide the session's access token in the request. If you are using our frontend SDK, this is done automatically via our frontend network interceptors. The access token will enable the backend to get a session and then link the social login account to session user. +- There are new types of failure scenarios when calling the signinup API which are not possible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#err_code_001) (> `ERR_CODE_008`). + +### Step 3: Extract the social login access token and user peofile info on the backend + +Once you call the signinup API from the frontend, SuperTokens will verify the OAuth tokens and fetch the user's profile info from the third party provider. SuperTokens will also link the newly created recipe user to the session user. + +To fetch the new user object and also the third party profile, you can override the signinup recipe function: + + + + +```tsx +import SuperTokens, { User } from "supertokens-node"; +import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: "EMAIL_OR_PHONE", // works with any contact metho + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // works with any flowType + // highlight-start + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // override the thirdparty sign in / up function + thirdPartySignInUp: async function (input) { + + let existingUser: User | undefined = undefined; + if (input.session !== undefined) { + existingUser = await SuperTokens.getUser(input.session.getUserId()); + } + + let response = await originalImplementation.thirdPartySignInUp(input); + + if (response.status === "OK") { + + let accessToken = response.oAuthTokens["access_token"]; + + let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; + + if (input.session !== undefined && response.user.id === input.session.getUserId()) { + if (response.user.loginMethods.length === existingUser!.loginMethods.length + 1) { + // new social account was linked to session user + } else { + // social account was already linked to the session + // user from before + } + } + } + + return response; + } + } + } + } + // highlight-end + }), + Session.init({ /* ... */ }) + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +Notice in the above snippet that we check for `input.session !== undefined && response.user.id === input.session.getUserId()`. This ensures that we run our custom logic only if it's linking a social account to your session account, and not during first factor login. + +## Adding a password to an existing account + +### Step 1: Enable account linking and emailpassword on the backend SDK + + + + +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +supertokens.init({ + supertokens: { + connectionURI: "...", + apiKey: "..." + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + recipeList: [ + // highlight-start + EmailPassword.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (user === undefined) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + if (session !== undefined && session.getUserId() === user.id) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true + } + } + return { + shouldAutomaticallyLink: false + } + } + }) + // highlight-end + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + + +In the above implementation of `shouldDoAutomaticAccountLinking`, we only allow account linking if the input session is present. This means that we are trying to link an email password account to an existing session user. Otherwise, we do not allow account linking which means that first factor account linking is disabled. If you want to enable that too, you can see [this page](./automatic-account-linking). + +### Step 2: Create a UI to show a password input to the user and handle the submit event + +:::important +If you want to use password based auth as a second factor, or for step up auth, see our docs in the [MFA recipe](/docs/mfa/introduction) instead. The guide below is only meant for if you want to add a password for a user and allow them to login via email password for first factor login. +::: + +First, you will need to detect if there already exists a password for the user. This can be done by inspecting the [user object](../../user-object) on the backend and checking if there is an `emailpassword` login method. + +Then, if no such login method exists, you will have to show a UI in which the user can add a password to their account. The default password validation rules can be found [here](/docs/emailpassword/common-customizations/signup-form/field-validators#changing-the-default-email-and-password-validators). + +You will also need to fetch the email of the user before you call the email password sign up API. Once again, you can fetch this using the the user object. If the user object does not have an email (which can only happen if the first factor is phone OTP), then you should ask the user to go through an email OTP flow (via out passwordless recipe) before asking them to set a password. Thge email OTP flow will also result in a passwordless user account being created and linked to the session user. + +Once you have the email on the frontend, you should call the [sign up API](/docs/emailpassword/custom-ui/email-password-login#sign-up-form). The two big differences in the implementation are: +- When you call the signup API, you need to provide the session's access token in the request. If you are using our frontend SDK, this is done automatically via our frontend network interceptors. The access token will enable the backend to get a session and then link the email password account to session user. +- There are new types of failure scenarios when calling the signup API which are not possible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#err_code_001) (> `ERR_CODE_008`). + +### Step 3: Checking for email match in the backend sign up API +Since the email is specified on the frontend, we want to verify it in the backend API before using it (since we shouldn't trust the frontend). This can be easily done by overriding the email password sign up API: + + + + +```tsx +import SuperTokens from "supertokens-node"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "..." + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + // highlight-start + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signUpPOST: async function (input) { + if (input.session !== undefined) { + // this means that we are trying to add a password to the session user + const inputEmail = input.formFields.find(f => f.id === "email")!.value; + let sessionUserId = input.session.getUserId(); + let userObject = await SuperTokens.getUser(sessionUserId); + if (userObject!.emails.find(e => e === inputEmail) === undefined) { + // this means that the input email does not belong to this user. + return { + status: "GENERAL_ERROR", + message: "Cannot use this email to add a password for this user" + } + } + } + return await originalImplementation.signUpPOST!(input); + } + } + } + } + // highlight-end + }), + Session.init({ /* ... */ }) + ] +}); +``` + + + + +:::note +Coming Soon +::: + + + + +:::note +Coming Soon +::: + + + \ No newline at end of file diff --git a/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx b/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx index 8e326cf2a..7967d1a3b 100644 --- a/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx +++ b/v2/thirdpartypasswordless/common-customizations/account-linking/automatic-account-linking.mdx @@ -32,11 +32,12 @@ You can enable this feature by providing the following callback implementation o import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { - connectionURI: "", - apiKey: "" + connectionURI: "...", + apiKey: "..." }, appInfo: { apiDomain: "...", @@ -46,7 +47,12 @@ supertokens.init({ recipeList: [ // highlight-start AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { + if (session !== undefined) { + return { + shouldAutomaticallyLink: false + } + } if (newAccountInfo.recipeUserId !== undefined && user !== undefined) { let userId = newAccountInfo.recipeUserId.getAsString(); let hasInfoAssociatedWithUserId = false // TODO: add your own implementation here. @@ -89,6 +95,7 @@ Coming Soon Notice that in case of `newAccountInfo.recipeUserId !== undefined && user !== undefined` being `true`, we add some extra logic to check if the user ID has any info associated with them in your application db. This is to prevent data loss for this user ID (see the [migtation section below](#migration-of-user-data-when-accounts-are-linked)). - `user: User | undefined`: If this is not `undefined`, it means that `newAccountInfo` user is about to linked to this `user`. If this is `undefined`, it means that `newAccountInfo` user is about to become a primary user. +- `session: SessionContainerInterface | undefined`: The session object of the user that is about to be linked. This is `undefined` if it's the first factor login. If a user has completed the first factor, and calls a sign up / in API again of some login method (either for MFA or for social login linking), then the `session` object will be defined. In the above snippet, we prevent account linking if a session is defined so that account linking only happens during the first factor login. - `tenant: string`: The ID of the tenant that the user is signing in / up to. - `userContext: any`: User defined userContext. @@ -98,18 +105,6 @@ Coming Soon You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not. -:::caution -If enabling automatic account linking for **an existing user base**, then before you enable it, you need to mark existing users as primary users so that future sign ups / ins with the same email / phone, but with a different login method, get linked to the existing user (as opposed to the other way around). - -To do this, you should create a script that: -- [Loops through all users](../user-pagination) in your app (oldest first). -- For each user, fetch the user's ID and call the [create primary user](./manual-account-linking#creating-a-primary-user) function on them **if their email is verified**. - - If at any point, you find that primary user creation failed because of `ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR` error, then it means that there is already another user with the same email / phone number that is already a primary user. In this case, you don't need to take any action for the current user (that failed) since they will be auto linked to the existing user when they sign in next with this method. As a side effect of this, their primary user ID will change when their account will be linked. To do any data migration at this point, see [this section on this page](#migration-of-user-data-when-accounts-are-linked). - - If you have two users with the same email / phone number, you can even pick which one of them will become a primary user based on your own logic. For example, instead of just making the oldest user the primary user, you can choose the one that has more activity on your app. -- You can also do the above via [direct API calls to the core](https://app.swaggerhub.com/apis/supertokens/CDI). -- When looping through users, and if you are using our managed service, make sure to add artificial delay in the loop to not breach [our rate limits](../../rate-limits). In case the API call or function calls throws an error with a `429`, please wait for a few seconds and then retry the call. -- In order to be able to do this, you will first need to enable account linking feature in the SuperTokens core. For self hosted core, you can get a license key by [signing up](https://supertokens.com/auth), and for managed service users, you can enable the feature [from the dashboard](https://supertokens.com/dashboard-saas). -::: ## Different scenarios of automatic account linking @@ -165,11 +160,12 @@ To prevent this scenario, you should: import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId, RecipeLevelUser } from "supertokens-node/recipe/accountlinking/types"; + import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { - connectionURI: "", - apiKey: "" + connectionURI: "...", + apiKey: "..." }, appInfo: { apiDomain: "...", @@ -178,7 +174,7 @@ To prevent this scenario, you should: }, recipeList: [ AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, userContext: any) => { + shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { return { shouldAutomaticallyLink: true, shouldRequireVerification: true @@ -373,5 +369,106 @@ The following is a list of support status codes that the end user might see duri - To resolve this issue, you can ask the user to try the reset password flow. +### ERR_CODE_014 +- This can happen when adding a password to an existing session user: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" + } + ``` +- An example scenario of when in the following scenario: + - Let's say that the app is confgured to not have autmatic account linking during the first factor. + - A user creates an email password account with email `e1`, verifies it, and links social login account to it with email `e2`. + - The user logs out, and then creates a social login account with email `e1`. Then, they are asked to add a password to this account. Since an email password account with `e1` already exists, SuperTokens will try and link that to this new account, but fail, since the email password account with `e1` is already a primary user. + +- To resolve this, we recommend that manually link the `e1` social login account with the `e1` email password account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. + +### ERR_CODE_015 +- This can happen when adding a password to an existing session user: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" + } + ``` +- An example scenario of when in the following scenario: + - A user creates a social login account with email `e1` which becomes a primary user. + - The user logs out, and creates another social login account with email `e2`, which also becomes a primary user. + - The user is asked to add a password for the new account with an option to also specify an email with it (this is strange, but theoritically possible). They now enter the email `e1` for the email password account. + - This will cause this type of error since the linking of the new social logn and email account will fail since there already exists another primary user with the same (`e1`) email. + +- To resolve this, we recommend not allowing users to specify an email when asking them to add a password for their account. + +### ERR_CODE_016 +- This can happen when adding a password to an existing session user: + - API Path is `/signup POST`. + - Output JSON: + ```json + { + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" + } + ``` +- An example scenario of when in the following scenario: + - Let's say that the app is configured to not have automatic account linking during the first factor. + - A user signs up with a social login account using Google with email `e1`, and they add another social account, with Facebook, with the same email. + - The user logs out and creates another social login account with email `e1` (say Github), and then tries and adds a password to this account with email `e1`. Here, SuperTokens will try and make the Github login a primary user, but fail, since the email `e1` is already a primary user (with Google login). + +- To resolve this, we recommend that manually link the `e1` Github social login account with the `e1` Google social login account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. + +### ERR_CODE_020 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_020)" + } + ``` +- This can happen when the thirdparty account that is trying to be linked to the session's account is not verified. It could happen when you are trying to associate a social login account to a user, but that social account's email is not verified (and if the email of that account is not the same as the current session's account's email). +- To resolve this, you can return `shouldRequireVerification` as `false` in the `shouldDoAutomaticAccountLinking` function implementation, or you can only allow users to link social login accounts that give verified accounts. + +### ERR_CODE_021 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_021)" + } + ``` +- This can happen when the thirdparty account that is trying to be linked to the session's account is already linked with another primary user. + +### ERR_CODE_022 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_022)" + } + ``` +- This can happen when the thirdparty account that is trying to be linked to the session's account has the same email as another primary user. + +### ERR_CODE_023 +- This can happen during association of a third party login to an existing session's account. + - API Path is `/signinup POST`. + - Output JSON: + ```json + { + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_023)" + } + ``` +- In order to link the third party user with the session user, we need to make sure that the session user is a primary user. However, that can fail if there exists another primary user with the same email as the session user, and in this case, this error is returned to the frontend. + ### Changing the error message on the frontend If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](../translations). diff --git a/v2/thirdpartypasswordless/common-customizations/change-magic-link-url.mdx b/v2/thirdpartypasswordless/common-customizations/change-magic-link-url.mdx index 40f9193ca..d124fa308 100644 --- a/v2/thirdpartypasswordless/common-customizations/change-magic-link-url.mdx +++ b/v2/thirdpartypasswordless/common-customizations/change-magic-link-url.mdx @@ -44,17 +44,14 @@ SuperTokens.init({ return { ...originalImplementation, sendEmail: async function (input) { - if (input.type === "PASSWORDLESS_LOGIN") { - return originalImplementation.sendEmail({ - ...input, - urlWithLinkCode: input.urlWithLinkCode?.replace( - // This is: `${websiteDomain}${websiteBasePath}/verify` - "http://localhost:3000/auth/verify", - "http://your.domain.com/your/path" - ) - }) - } - return originalImplementation.sendEmail(input); + return originalImplementation.sendEmail({ + ...input, + urlWithLinkCode: input.urlWithLinkCode?.replace( + // This is: `${websiteDomain}${websiteBasePath}/verify` + "http://localhost:3000/auth/verify", + "http://your.domain.com/your/path" + ) + }) } } } diff --git a/v2/thirdpartypasswordless/common-customizations/embed-sign-in-up-form.mdx b/v2/thirdpartypasswordless/common-customizations/embed-sign-in-up-form.mdx index eb0066796..cd6af165b 100644 --- a/v2/thirdpartypasswordless/common-customizations/embed-sign-in-up-form.mdx +++ b/v2/thirdpartypasswordless/common-customizations/embed-sign-in-up-form.mdx @@ -56,21 +56,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -120,21 +115,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -186,21 +176,16 @@ SuperTokens.init({ // highlight-next-line disableDefaultUI: true, // This will prevent SuperTokens from displaying the default login UI in the `/auth` page. }, - // highlight-start - async getRedirectionURL(context) { - if (context.action === "SUCCESS") { - return "/dashboard"; // defaults to "/" - }; - }, - // highlight-end // ... }), // ... ], // highlight-start async getRedirectionURL(context) { - // The user will be taken to this path when they need to login. - if (context.action === "TO_AUTH") { + if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } else if (context.action === "TO_AUTH") { + // The user will be taken to this path when they need to login. return "/auth"; // return the path where you are rendering the Auth UI }; }, @@ -226,8 +211,7 @@ function AuthPage() { In the above code snippet, we: 1. Disabled the default Auth UI by setting `disableDefaultUI` to `true` inside the ThirdPartyPasswordless recipe config. -2. Overrode the `getRedirectionURL` function inside the ThirdPartyPasswordless recipe config to redirect to `/dashboard` upon successful login. -3. Overrode the `getRedirectionURL` function inside the SuperTokens config to redirect to `/auth` when login is required. +2. Override the `getRedirectionURL` function inside the ThirdPartyPasswordless recipe config to redirect to `/dashboard` upon successful login and to `/auth` when login is required. Feel free to customize the redirection URLs as needed. diff --git a/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx b/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx index 7822eadaa..75be8c98e 100644 --- a/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx +++ b/v2/thirdpartypasswordless/common-customizations/handling-signinup-success.mdx @@ -130,10 +130,12 @@ SuperTokens.init({ let providerAccessToken = response.oAuthTokens["access_token"]; let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // TODO: Post sign up logic - } else { - // TODO: Post sign in logic + if (input.session === undefined) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // TODO: Post sign up logic + } else { + // TODO: Post sign in logic + } } } @@ -152,10 +154,12 @@ SuperTokens.init({ const { id, emails } = response.user; } - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // TODO: post sign up logic - } else { - // TODO: post sign in logic + if (input.session === undefined) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // TODO: post sign up logic + } else { + // TODO: post sign in logic + } } } return response; diff --git a/v2/thirdpartypasswordless/common-customizations/multi-tenancy/common-domain-login.mdx b/v2/thirdpartypasswordless/common-customizations/multi-tenancy/common-domain-login.mdx index 6d8317724..9abbe01d6 100644 --- a/v2/thirdpartypasswordless/common-customizations/multi-tenancy/common-domain-login.mdx +++ b/v2/thirdpartypasswordless/common-customizations/multi-tenancy/common-domain-login.mdx @@ -289,7 +289,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button @@ -297,7 +297,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { // more checks for other providers } } - if (loginMethods.passwordless.enabled) { + if (loginMethods.firstFactors.includes("emailpassword")) { // render sign in with passwordless UI as well. } // more checks for other login methods... @@ -314,7 +314,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button @@ -322,7 +322,10 @@ async function fetchLoginMethodsForTenant(tenantId: string) { // more checks for other providers } } - if (loginMethods.passwordless.enabled) { + if (loginMethods.firstFactors.includes("otp-email") || + loginMethods.firstFactors.includes("otp-sms") || + loginMethods.firstFactors.includes("link-email") || + loginMethods.firstFactors.includes("link-sms")) { // render sign in with passwordless UI as well. } // more checks for other login methods... @@ -671,23 +674,25 @@ SuperTokens.init({ appName: "...", websiteDomain: "...", }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + let claimValue: string[] | undefined = await Session.getClaimValue({ + claim: Multitenancy.AllowedDomainsClaim + }); + if (claimValue !== undefined) { + window.location.href = "https://" + claimValue[0]; + } else { + // there was no configured allowed domain for this user. Throw an error cause of + // misconfig or redirect to a default sub domain + } + } + return undefined; + }, + // highlight-end recipeList: [ ^{recipeNameCapitalLetters}.init({ ^{reactRecipeInitDefault} - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { - let claimValue: string[] | undefined = await Session.getClaimValue({ - claim: Multitenancy.AllowedDomainsClaim - }); - if (claimValue !== undefined) { - window.location.href = "https://" + claimValue[0]; - } else { - // there was no configured allowed domain for this user. Throw an error cause of - // misconfig or redirect to a default sub domain - } - } - return undefined; - } }), ] }); diff --git a/v2/thirdpartypasswordless/common-customizations/multi-tenancy/sub-domain-login.mdx b/v2/thirdpartypasswordless/common-customizations/multi-tenancy/sub-domain-login.mdx index 0b24e21ba..fadf1c2e3 100644 --- a/v2/thirdpartypasswordless/common-customizations/multi-tenancy/sub-domain-login.mdx +++ b/v2/thirdpartypasswordless/common-customizations/multi-tenancy/sub-domain-login.mdx @@ -166,7 +166,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button @@ -174,7 +174,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { // more checks for other providers } } - if (loginMethods.passwordless.enabled) { + if (loginMethods.firstFactors.includes("emailpassword")) { // render sign in with passwordless UI as well. } // more checks for other login methods... @@ -191,7 +191,7 @@ async function fetchLoginMethodsForTenant(tenantId: string) { tenantId }) - if (loginMethods.thirdParty.enabled) { + if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find(i => i.id === "active-directory")) { // render sign in with Active Directory button @@ -199,7 +199,10 @@ async function fetchLoginMethodsForTenant(tenantId: string) { // more checks for other providers } } - if (loginMethods.passwordless.enabled) { + if (loginMethods.firstFactors.includes("otp-email") || + loginMethods.firstFactors.includes("otp-sms") || + loginMethods.firstFactors.includes("link-email") || + loginMethods.firstFactors.includes("link-sms")) { // render sign in with passwordless UI as well. } // more checks for other login methods... diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx index 8900a22f7..545e51ca3 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/claims/claim-validators.mdx @@ -52,17 +52,20 @@ Without a special construct of session claim validators, the updating of the ses Before we dive deep into claim validators, let's talk about session claim objects. These are objects that conform to an interface that allows SuperTokens to automatically add session claims to the access token payload. Here is the interface: ```tsx + +import { RecipeUserId } from "supertokens-node"; + type JSONObject = any; // typecheck-only, removed from output interface SessionClaim { readonly key: string; - fetchValue(userId: string, tenantId: string, userContext: any): Promise; + fetchValue(userId: string, recipeUserId: RecipeUserId, tenantId: string, currentPayload: JSONObject | undefined, userContext: any): Promise; addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject; - removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayloadByMerge_internal(payload: JSONObject, userContext: any): JSONObject; - removeFromPayload(payload: JSONObject, userContext?: any): JSONObject; + removeFromPayload(payload: JSONObject, userContext: any): JSONObject; getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; @@ -364,7 +367,7 @@ SuperTokens.init({ input.accessTokenPayload = { ...input.accessTokenPayload, // highlight-next-line - ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)) + ...(await UserRoleClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)) }; /* @@ -509,8 +512,8 @@ The `build` function is a helper function which all claims have that does the fo ```text class Claim { // other functions like fetchValue, getValueFromPayload etc.. - function build(userId, tenantId) { - claimValue = this.fetchValue(userId, tenantId); + function build(userId, recipeUserId, tenantId) { + claimValue = this.fetchValue(userId, recipeUserId, tenantId); return this.addToPayload_internal({}, claimValue) } } diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/fetch-sessions-for-user.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/fetch-sessions-for-user.mdx index 989d8c1cb..cfec26595 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/fetch-sessions-for-user.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/fetch-sessions-for-user.mdx @@ -121,6 +121,6 @@ for session_handle in session_handles: -By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to delete the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. +By default, getAllSessionHandlesForUser will fetch all the sessionHandles for the user across all the tenants. If you want to fetch the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/jwt-signing-key-rotation.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/jwt-signing-key-rotation.mdx index d5a198254..dd357fd96 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/jwt-signing-key-rotation.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/jwt-signing-key-rotation.mdx @@ -90,6 +90,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -114,6 +119,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -134,9 +152,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -147,7 +162,10 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/revoke-session.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/revoke-session.mdx index 3b7acdf47..c474e8976 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/revoke-session.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/revoke-session.mdx @@ -46,7 +46,7 @@ function NavBar() { async function onLogout() { // highlight-next-line await signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } return (
                      @@ -71,7 +71,7 @@ import Session from "supertokens-auth-react/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -101,7 +101,7 @@ import Session from "supertokens-web-js/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` @@ -113,7 +113,7 @@ import supertokensSession from "supertokens-web-js-script/recipe/session"; async function logout () { // highlight-next-line await supertokensSession.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or redirect to wherever the login page is } ``` diff --git a/v2/thirdpartypasswordless/common-customizations/sessions/with-jwt/jwt-verification.mdx b/v2/thirdpartypasswordless/common-customizations/sessions/with-jwt/jwt-verification.mdx index c986e3b97..d1f0e905a 100644 --- a/v2/thirdpartypasswordless/common-customizations/sessions/with-jwt/jwt-verification.mdx +++ b/v2/thirdpartypasswordless/common-customizations/sessions/with-jwt/jwt-verification.mdx @@ -170,6 +170,11 @@ SuperTokens.init({ ] }); ``` + +:::caution +Updating this value will cause a spike in the session refresh API, as and when users visit your application. +::: + @@ -194,6 +199,19 @@ func main() { }) } ``` + +:::caution +Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): + +If `useDynamicAccessTokenSigningKey` is false: +- `UPDATE session_info SET use_static_key = true;` + +Else if `useDynamicAccessTokenSigningKey` is true: +- `UPDATE session_info SET use_static_key = false;` + +If you are using the managed core, you can send an email to us about this, and we will run the query for you. +::: + @@ -214,9 +232,6 @@ init( ) ``` - - - :::caution Once you make the change to this boolean, you will need to run the following query in your database for it to take affect for existing sessions (otherwise those users will be stuck in an infinite refresh loop): @@ -227,9 +242,12 @@ Else if `useDynamicAccessTokenSigningKey` is true: - `UPDATE session_info SET use_static_key = false;` If you are using the managed core, you can send an email to us about this, and we will run the query for you. - ::: + + + + diff --git a/v2/thirdpartypasswordless/common-customizations/userid-format.mdx b/v2/thirdpartypasswordless/common-customizations/userid-format.mdx index 104e5107b..1eafcb724 100644 --- a/v2/thirdpartypasswordless/common-customizations/userid-format.mdx +++ b/v2/thirdpartypasswordless/common-customizations/userid-format.mdx @@ -61,7 +61,7 @@ SuperTokens.init({ let providerAccessToken = response.oAuthTokens["access_token"]; let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1 && input.session === undefined) { // highlight-start let externalUserId = "" await SuperTokens.createUserIdMapping({ superTokensUserId: response.user.id, externalUserId }) @@ -88,7 +88,7 @@ SuperTokens.init({ const { id, emails } = response.user; } - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1 && input.session === undefined) { // highlight-start let externalUserId = "" await SuperTokens.createUserIdMapping({ superTokensUserId: response.user.id, externalUserId }) diff --git a/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx b/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx index b385f8ea6..91a8894db 100644 --- a/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx +++ b/v2/thirdpartypasswordless/custom-ui/login-magic-link.mdx @@ -54,7 +54,10 @@ async function sendMagicLink(email: string) { */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. See that section in our docs. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // Magic link sent successfully. window.alert("Please check your email for the magic link"); @@ -91,7 +94,10 @@ async function sendMagicLink(email: string) { */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. See that section in our docs. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // Magic link sent successfully. window.alert("Please check your email for the magic link"); @@ -143,7 +149,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup/co The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the magic link was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend, or if the input email or password failed the backend validation logic. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. The response from the API call is the following object (in case of `status: "OK"`): ```json @@ -196,17 +202,14 @@ SuperTokens.init({ return { ...originalImplementation, sendEmail: async function (input) { - if (input.type === "PASSWORDLESS_LOGIN") { - return originalImplementation.sendEmail({ - ...input, - urlWithLinkCode: input.urlWithLinkCode?.replace( - // This is: `${websiteDomain}${websiteBasePath}/verify` - "http://localhost:3000/auth/verify", - "http://your.domain.com/your/path" - ) - }) - } - return originalImplementation.sendEmail(input); + return originalImplementation.sendEmail({ + ...input, + urlWithLinkCode: input.urlWithLinkCode?.replace( + // This is: `${websiteDomain}${websiteBasePath}/verify` + "http://localhost:3000/auth/verify", + "http://your.domain.com/your/path" + ) + }) } } } @@ -633,7 +636,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" | "RESTART_FLOW_ERROR"`: These responses indicate that the Magic link was invalid or expired. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. diff --git a/v2/thirdpartypasswordless/custom-ui/login-otp.mdx b/v2/thirdpartypasswordless/custom-ui/login-otp.mdx index 8ee19c674..f3f851217 100644 --- a/v2/thirdpartypasswordless/custom-ui/login-otp.mdx +++ b/v2/thirdpartypasswordless/custom-ui/login-otp.mdx @@ -53,7 +53,10 @@ async function sendOTP(email: string) { */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. See that section in our docs. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // OTP sent successfully. window.alert("Please check your email for an OTP"); @@ -90,7 +93,10 @@ async function sendOTP(email: string) { */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. See that section in our docs. + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // OTP sent successfully. window.alert("Please check your email for an OTP"); @@ -142,7 +148,7 @@ curl --location --request POST '^{form_apiDomain}^{form_apiBasePath}/signinup/co The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the OTP was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend, or if the input email or password failed the backend validation logic. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. The response from the API call is the following object (in case of `status: "OK"`): ```json @@ -452,7 +458,7 @@ The response body from the API call has a `status` property in it: - `status: "EXPIRED_USER_INPUT_CODE_ERROR"`: The entered OTP is too old. You should ask the user to resend a new OTP and try again. - `status: "RESTART_FLOW_ERROR"`: These responses that the user tried invalid OTPs too many times. - `status: "GENERAL_ERROR"`: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. See that section in our docs. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. diff --git a/v2/thirdpartypasswordless/custom-ui/sign-out.mdx b/v2/thirdpartypasswordless/custom-ui/sign-out.mdx index c851395f9..af9169fa0 100644 --- a/v2/thirdpartypasswordless/custom-ui/sign-out.mdx +++ b/v2/thirdpartypasswordless/custom-ui/sign-out.mdx @@ -28,7 +28,7 @@ import Session from "supertokens-web-js/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` @@ -40,7 +40,7 @@ import supertokensSession from "supertokens-web-js-script/recipe/session"; async function logout () { // highlight-next-line await supertokensSession.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` diff --git a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx index bfc49de02..88df6c6dd 100644 --- a/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx +++ b/v2/thirdpartypasswordless/custom-ui/thirdparty-login.mdx @@ -138,7 +138,10 @@ Once the third party provider redirects your user back to your app, you need to } window.location.assign("/home"); } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. Please see our account linking docs + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -178,7 +181,10 @@ Once the third party provider redirects your user back to your app, you need to } window.location.assign("/home"); } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // this can happen due to automatic account linking. Please see our account linking docs + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason) } else { // SuperTokens requires that the third party provider // gives an email for the user. If that's not the case, sign up / in @@ -630,7 +636,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -838,7 +844,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -918,7 +924,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. @@ -969,7 +975,7 @@ The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This is returned if the social / SSO provider did not provider an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. - `status: "GENERAL_ERROR"`: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen due to automatic account linking. Please see our docs for account linking for more information. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note On success, the backend will send back session tokens as part of the response headers which will be automatically handled by our frontend SDK for you. diff --git a/v2/thirdpartypasswordless/email-delivery/custom-method.mdx b/v2/thirdpartypasswordless/email-delivery/custom-method.mdx index e308fb542..703d2a45b 100644 --- a/v2/thirdpartypasswordless/email-delivery/custom-method.mdx +++ b/v2/thirdpartypasswordless/email-delivery/custom-method.mdx @@ -39,13 +39,22 @@ supertokens.init({ ...originalImplementation, sendEmail: async function (input) { let { + isFirstFactor, codeLifetime, // amount of time the code is alive for (in MS) email, urlWithLinkCode, // magic link userInputCode, // OTP } = input; - // TODO: create and send email + if (isFirstFactor) { + // this is for first factor login + // TODO: create and send email + } else { + // this is for MFA login (only applicable if you are using MFA). + // In this case, the urlWithLinkCode will always be undefined since + // we only support OTP based MFA and not link based MFA + // TODO: create and send email + } } } } diff --git a/v2/thirdpartypasswordless/email-delivery/smtp/change-email-content.mdx b/v2/thirdpartypasswordless/email-delivery/smtp/change-email-content.mdx index 66ea2f259..e1fa90e0c 100644 --- a/v2/thirdpartypasswordless/email-delivery/smtp/change-email-content.mdx +++ b/v2/thirdpartypasswordless/email-delivery/smtp/change-email-content.mdx @@ -47,19 +47,41 @@ supertokens.init({ ...originalImplementation, getContent: async function (input) { let { + isFirstFactor, codeLifetime, // amount of time the code is alive for (in MS) email, urlWithLinkCode, // magic link userInputCode, // OTP } = input; - // send some custom email content - return { - body: "EMAIL BODY", - isHtml: true, - subject: "Some subject", - toEmail: email + if (isFirstFactor) { + // this is for first factor login + return { + body: "EMAIL BODY", + isHtml: true, + subject: "Login to your account", + toEmail: email + } + } else { + // this is for MFA login (only applicable if you are using MFA). + // In this case, the urlWithLinkCode will always be undefined since + // we only support OTP based MFA and not link based MFA + return { + body: "EMAIL BODY", + isHtml: true, + subject: "Login via MFA", + toEmail: email + } } + + // You can even call the original implementation and + // modify its content: + + /* + let originalContent = await originalImplementation.getContent(input) + originalContent.subject = "My custom subject"; + return originalContent; + */ } } } diff --git a/v2/thirdpartypasswordless/mfa.mdx b/v2/thirdpartypasswordless/mfa.mdx new file mode 100644 index 000000000..1c72b293a --- /dev/null +++ b/v2/thirdpartypasswordless/mfa.mdx @@ -0,0 +1,9 @@ +--- +id: mfa +title: Multi factor auth +hide_title: true +--- + +# Multi factor auth + +See our guide for Multi Factor Auth [here](/docs/mfa/introduction). \ No newline at end of file diff --git a/v2/thirdpartypasswordless/migration/about.mdx b/v2/thirdpartypasswordless/migration/about.mdx index ed9b1b976..08b0ef756 100644 --- a/v2/thirdpartypasswordless/migration/about.mdx +++ b/v2/thirdpartypasswordless/migration/about.mdx @@ -45,3 +45,6 @@ There are 3 steps to user migration: - This will prevent users from having to re-authenticate. You can learn more about how to implement these changes in the [Session Migration](./session-migration) section. + +## Step 4) MFA migration +If you are using MFA in your app, checkout the MFA migration section [here](/docs/mfa/migration) after you have gone through the above migration steps. diff --git a/v2/thirdpartypasswordless/migration/mfa-migration.mdx b/v2/thirdpartypasswordless/migration/mfa-migration.mdx new file mode 100644 index 000000000..85f7c646b --- /dev/null +++ b/v2/thirdpartypasswordless/migration/mfa-migration.mdx @@ -0,0 +1,12 @@ +--- +id: mfa-migration +title: Step 4) MFA migration +hide_title: true +--- + + + + +# MFA migration + +If you are using MFA in your app, checkout the MFA migration section [here](/docs/mfa/migration) after you have gone through the previous steps in migration. diff --git a/v2/thirdpartypasswordless/multi-tenant.mdx b/v2/thirdpartypasswordless/multi-tenant.mdx new file mode 100644 index 000000000..42d32e856 --- /dev/null +++ b/v2/thirdpartypasswordless/multi-tenant.mdx @@ -0,0 +1,9 @@ +--- +id: multi-tenant +title: Multi tenancy / B2B orgs +hide_title: true +--- + +# Multi tenancy / B2B orgs + +See our guide for multi tenancy auth [here](/docs/multitenancy/introduction). \ No newline at end of file diff --git a/v2/thirdpartypasswordless/nextjs/app-directory/setting-up-frontend.mdx b/v2/thirdpartypasswordless/nextjs/app-directory/setting-up-frontend.mdx index 31028dab7..68c551eac 100644 --- a/v2/thirdpartypasswordless/nextjs/app-directory/setting-up-frontend.mdx +++ b/v2/thirdpartypasswordless/nextjs/app-directory/setting-up-frontend.mdx @@ -62,7 +62,9 @@ export default function Auth() { ## 3) Visit `/auth` page on your website -If you see a login UI, then you have successfully completed this step! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) +If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre built UI, for each page on [this link](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/auth-page--playground). + +If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/v2/thirdpartypasswordless/nextjs/setting-up-frontend.mdx b/v2/thirdpartypasswordless/nextjs/setting-up-frontend.mdx index 9f7cf1295..e1f0a9e94 100644 --- a/v2/thirdpartypasswordless/nextjs/setting-up-frontend.mdx +++ b/v2/thirdpartypasswordless/nextjs/setting-up-frontend.mdx @@ -59,7 +59,9 @@ export default function Auth() { ## 3) Visit `/auth` page on your website -If you see a login UI, then you have successfully completed this step! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) +If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre built UI, for each page on [this link](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/auth-page--playground). + +If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/v2/thirdpartypasswordless/pre-built-ui/auth-redirection.mdx b/v2/thirdpartypasswordless/pre-built-ui/auth-redirection.mdx index c5ca52af0..7495a1784 100644 --- a/v2/thirdpartypasswordless/pre-built-ui/auth-redirection.mdx +++ b/v2/thirdpartypasswordless/pre-built-ui/auth-redirection.mdx @@ -30,26 +30,26 @@ SuperTokens.init({ appName: "...", websiteDomain: "...", }, + // highlight-start + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + if (context.redirectToPath !== undefined) { + // we are navigating back to where the user was before they authenticated + return context.redirectToPath; + } + if (context.createdNewUser) { + // user signed up + } else { + // user signed in + } + return "/dashboard"; + } + return undefined; + }, + // highlight-end recipeList: [ ^{recipeNameCapitalLetters}.init({ ^{reactRecipeInitDefault} // typecheck-only, removed from output - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { - if (context.redirectToPath !== undefined) { - // we are navigating back to where the user was before they authenticated - return context.redirectToPath; - } - if (context.isNewPrimaryUser) { - // user signed up - } else { - // user signed in - } - return "/dashboard"; - } - return undefined; - } - // highlight-end }), ] }); diff --git a/v2/thirdpartypasswordless/pre-built-ui/sign-out.mdx b/v2/thirdpartypasswordless/pre-built-ui/sign-out.mdx index 6cf670151..ed1a43dca 100644 --- a/v2/thirdpartypasswordless/pre-built-ui/sign-out.mdx +++ b/v2/thirdpartypasswordless/pre-built-ui/sign-out.mdx @@ -26,7 +26,7 @@ function NavBar() { async function onLogout() { // highlight-next-line await signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } return (
                        @@ -51,7 +51,7 @@ import Session from "supertokens-auth-react/recipe/session"; async function logout () { // highlight-next-line await Session.signOut(); - window.location.href = "/"; + window.location.href = "/auth"; // or to wherever your logic page is } ``` diff --git a/v2/thirdpartypasswordless/sidebars.js b/v2/thirdpartypasswordless/sidebars.js index 91a70a3dd..97332dfc4 100644 --- a/v2/thirdpartypasswordless/sidebars.js +++ b/v2/thirdpartypasswordless/sidebars.js @@ -496,7 +496,8 @@ module.exports = { "common-customizations/account-linking/overview", "common-customizations/account-linking/automatic-account-linking", "common-customizations/account-linking/manual-account-linking", - "common-customizations/account-linking/security-considerations" + "common-customizations/account-linking/security-considerations", + "common-customizations/account-linking/adding-accounts-to-session" ] }, { @@ -709,6 +710,8 @@ module.exports = { "user-roles/delete-roles", ], }, + "mfa", + "multi-tenant" ] }, "rate-limits", @@ -736,7 +739,8 @@ module.exports = { ], }, "migration/data-migration", - "migration/session-migration" + "migration/session-migration", + "migration/mfa-migration" ], }, { diff --git a/v2/thirdpartypasswordless/sms-delivery/twilio/change-sms-content.mdx b/v2/thirdpartypasswordless/sms-delivery/twilio/change-sms-content.mdx index b6eaf61ff..a4e610739 100644 --- a/v2/thirdpartypasswordless/sms-delivery/twilio/change-sms-content.mdx +++ b/v2/thirdpartypasswordless/sms-delivery/twilio/change-sms-content.mdx @@ -43,15 +43,25 @@ supertokens.init({ return { ...originalImplementation, getContent: async function ({ + isFirstFactor, codeLifetime, // amount of time the code is alive for (in MS) phoneNumber, urlWithLinkCode, // magic link userInputCode, // OTP }) { - // send some custom SMS content - return { - toPhoneNumber: phoneNumber, - body: "SMS BODY" + if (isFirstFactor) { + // send some custom SMS content + return { + toPhoneNumber: phoneNumber, + body: "SMS BODY" + } + } else { + // for second factor, urlWithLinkCode will always be + // undefined since we only support OTP based for second factor + return { + toPhoneNumber: phoneNumber, + body: "SMS BODY" + } } // You can even call the original implementation and diff --git a/v2/thirdpartypasswordless/supabase-intergration/backend-signup-override.mdx b/v2/thirdpartypasswordless/supabase-intergration/backend-signup-override.mdx index a4db5a0a1..198412267 100644 --- a/v2/thirdpartypasswordless/supabase-intergration/backend-signup-override.mdx +++ b/v2/thirdpartypasswordless/supabase-intergration/backend-signup-override.mdx @@ -49,7 +49,7 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.consumeCodePOST(input); - if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1 && input.session === undefined) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); @@ -139,7 +139,7 @@ let backendConfig = (): TypeInput => { let response = await originalImplementation.thirdPartySignInUpPOST(input); // check that there is no issue with sign up and that a new user is created - if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + if (response.status === "OK" && response.createdNewRecipeUser && response.user.loginMethods.length === 1 && input.session === undefined) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload();