Skip to content

Commit

Permalink
Merge pull request #763 from supertokens/mfa
Browse files Browse the repository at this point in the history
MFA docs
  • Loading branch information
rishabhpoddar authored Mar 12, 2024
2 parents 721feb7 + 0add4b1 commit 623096a
Show file tree
Hide file tree
Showing 192 changed files with 11,957 additions and 886 deletions.
44 changes: 44 additions & 0 deletions v2/change_me/ui-switcher.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,50 @@ show_ui_switcher: true

import {PreBuiltOrCustomUISwitcher, PreBuiltUIContent, CustomUIContent} from "/src/components/preBuiltOrCustomUISwitcher"

<PreBuiltOrCustomUISwitcher>

<PreBuiltUIContent>

## Pre build UI Last heading



<FrontendPreBuiltUITabs>
<TabItem value="reactjs">

TODO... React

</TabItem>

<TabItem value="angular">

TODO... Angular

</TabItem>

<TabItem value="vue">

TODO... Vue

</TabItem>

</FrontendPreBuiltUITabs>



</PreBuiltUIContent>

<CustomUIContent>

## Custom UI Last heading

</CustomUIContent>

</PreBuiltOrCustomUISwitcher>




<PreBuiltOrCustomUISwitcher>

<PreBuiltUIContent>
Expand Down
11 changes: 11 additions & 0 deletions v2/community/reusableMD/mfa/MFAPaidBanner.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import CustomAdmonition from "/src/components/customAdmonition"

<CustomAdmonition type="paid-feature">

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.

</CustomAdmonition>
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
})
]
});
```
</TabItem>
Expand Down
2 changes: 1 addition & 1 deletion v2/emailpassword/advanced-customizations/user-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

<AccountLinkingPaidBanner />

# 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

<BackendSDKTabs>
<TabItem value="nodejs">

```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
]
});
```

</TabItem>
<TabItem value="go">

:::note
Coming Soon
:::

</TabItem>
<TabItem value="python">

:::note
Coming Soon
:::

</TabItem>
</BackendSDKTabs>

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:

<BackendSDKTabs>
<TabItem value="nodejs">

```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({ /* ... */ })
]
});
```

</TabItem>
<TabItem value="go">

:::note
Coming Soon
:::

</TabItem>
<TabItem value="python">

:::note
Coming Soon
:::

</TabItem>
</BackendSDKTabs>

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.
2 changes: 1 addition & 1 deletion v2/emailpassword/common-customizations/change-password.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 623096a

Please sign in to comment.