Skip to content

Commit 623096a

Browse files
Merge pull request #763 from supertokens/mfa
MFA docs
2 parents 721feb7 + 0add4b1 commit 623096a

File tree

192 files changed

+11957
-886
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

192 files changed

+11957
-886
lines changed

v2/change_me/ui-switcher.mdx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,50 @@ show_ui_switcher: true
99

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

12+
<PreBuiltOrCustomUISwitcher>
13+
14+
<PreBuiltUIContent>
15+
16+
## Pre build UI Last heading
17+
18+
19+
20+
<FrontendPreBuiltUITabs>
21+
<TabItem value="reactjs">
22+
23+
TODO... React
24+
25+
</TabItem>
26+
27+
<TabItem value="angular">
28+
29+
TODO... Angular
30+
31+
</TabItem>
32+
33+
<TabItem value="vue">
34+
35+
TODO... Vue
36+
37+
</TabItem>
38+
39+
</FrontendPreBuiltUITabs>
40+
41+
42+
43+
</PreBuiltUIContent>
44+
45+
<CustomUIContent>
46+
47+
## Custom UI Last heading
48+
49+
</CustomUIContent>
50+
51+
</PreBuiltOrCustomUISwitcher>
52+
53+
54+
55+
1256
<PreBuiltOrCustomUISwitcher>
1357

1458
<PreBuiltUIContent>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import CustomAdmonition from "/src/components/customAdmonition"
2+
3+
<CustomAdmonition type="paid-feature">
4+
5+
This is a paid feature.
6+
7+
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.
8+
9+
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.
10+
11+
</CustomAdmonition>

v2/emailpassword/advanced-customizations/frontend-hooks/handle-event.mdx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@ EmailPassword.init({
2727
// called when a user visits the login / sign up page with a valid session
2828
// in this case, they are usually redirected to the main app
2929
} else if (context.action === "SUCCESS") {
30-
let user = context.user;
31-
if (context.isNewRecipeUser && context.user.loginMethods.length === 1) {
32-
// sign up success
30+
if (context.createdNewSession) {
31+
let user = context.user;
32+
if (context.isNewRecipeUser && context.user.loginMethods.length === 1) {
33+
// sign up success
34+
} else {
35+
// sign in success
36+
}
3337
} else {
34-
// sign in success
38+
// this is during second factor login of step up auth flow
3539
}
3640
}
3741
}

v2/emailpassword/advanced-customizations/frontend-hooks/redirection-callback.mdx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,28 @@ This function is used to change where the user is redirected to post certain act
1616

1717
```tsx
1818
import EmailPassword from "supertokens-auth-react/recipe/emailpassword";
19+
import SuperTokens from "supertokens-auth-react";
1920

20-
EmailPassword.init({
21+
SuperTokens.init({
22+
appInfo: {
23+
appName: "SuperTokens",
24+
apiDomain: "http://localhost:3000",
25+
websiteDomain: "http://localhost:3000",
26+
},
2127
getRedirectionURL: async (context) => {
22-
if (context.action === "RESET_PASSWORD") {
23-
// called when the user clicked on the forgot password button
24-
} else if (context.action === "SUCCESS") {
25-
// called on a successful sign in / up. Where should the user go next?
28+
if (context.action === "TO_AUTH") {
29+
// This is called when we want to navigate to the login page.
30+
// By default, this will go to the configured websiteBasePath (/auth)
31+
} else if (context.action === "SUCCESS" && context.newSessionCreated) {
32+
// This is called when the user has successfully signed in / signed up.
33+
// By default, this will go to "/" or to
34+
// the redirectToPath if it is set (the page from which the user was redirected to the auth page).
2635
let redirectToPath = context.redirectToPath;
2736
if (redirectToPath !== undefined) {
2837
// we are navigating back to where the user was before they authenticated
2938
return redirectToPath;
3039
}
31-
if (context.isNewPrimaryUser) {
40+
if (context.createdNewUser) {
3241
// user signed up
3342
return "/onboarding"
3443
} else {
@@ -38,7 +47,18 @@ EmailPassword.init({
3847
}
3948
// return undefined to let the default behaviour play out
4049
return undefined;
41-
}
50+
},
51+
recipeList: [
52+
EmailPassword.init({
53+
getRedirectionURL: async (context) => {
54+
if (context.action === "RESET_PASSWORD") {
55+
// called when the user clicked on the forgot password button
56+
}
57+
// return undefined to let the default behaviour play out
58+
return undefined;
59+
}
60+
})
61+
]
4262
});
4363
```
4464
</TabItem>

v2/emailpassword/advanced-customizations/user-context.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ SuperTokens.init({
5454
// override sign up using email / password
5555
signUp: async function (input) {
5656
let resp = await originalImplementation.signUp(input);
57-
if (resp.status === "OK" && resp.user.loginMethods.length === 1) {
57+
if (resp.status === "OK" && resp.user.loginMethods.length === 1 && input.session === undefined) {
5858
/*
5959
* This is called during the sign up API for email password login,
6060
* but before calling the createNewSession function.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
---
2+
id: adding-accounts-to-session
3+
title: Linking social accounts to an existing account
4+
hide_title: true
5+
---
6+
7+
import AccountLinkingPaidBanner from '../../../community/reusableMD/accountlinking/AccountLinkingPaidBanner.mdx'
8+
import BackendSDKTabs from "/src/components/tabs/BackendSDKTabs";
9+
10+
<AccountLinkingPaidBanner />
11+
12+
# Linking social accounts to an existing account
13+
14+
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.
15+
16+
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.
17+
18+
:::caution
19+
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.
20+
21+
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.
22+
:::
23+
24+
## Linking a social account to an existing user account
25+
26+
### Step 1: Enable account linking and third party on the backend SDK
27+
28+
<BackendSDKTabs>
29+
<TabItem value="nodejs">
30+
31+
```tsx
32+
import supertokens, { User, RecipeUserId } from "supertokens-node";
33+
import AccountLinking from "supertokens-node/recipe/accountlinking";
34+
import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types";
35+
import { SessionContainerInterface } from "supertokens-node/recipe/session/types";
36+
import ThirdParty from "supertokens-node/recipe/thirdparty";
37+
38+
supertokens.init({
39+
supertokens: {
40+
connectionURI: "...",
41+
apiKey: "..."
42+
},
43+
appInfo: {
44+
apiDomain: "...",
45+
appName: "...",
46+
websiteDomain: "..."
47+
},
48+
recipeList: [
49+
// highlight-start
50+
ThirdParty.init({ /* ...*/ }),
51+
AccountLinking.init({
52+
shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => {
53+
if (user === undefined) {
54+
return {
55+
shouldAutomaticallyLink: true,
56+
shouldRequireVerification: true
57+
}
58+
}
59+
if (session !== undefined && session.getUserId() === user.id) {
60+
return {
61+
shouldAutomaticallyLink: true,
62+
shouldRequireVerification: true
63+
}
64+
}
65+
return {
66+
shouldAutomaticallyLink: false
67+
}
68+
}
69+
})
70+
// highlight-end
71+
]
72+
});
73+
```
74+
75+
</TabItem>
76+
<TabItem value="go">
77+
78+
:::note
79+
Coming Soon
80+
:::
81+
82+
</TabItem>
83+
<TabItem value="python">
84+
85+
:::note
86+
Coming Soon
87+
:::
88+
89+
</TabItem>
90+
</BackendSDKTabs>
91+
92+
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).
93+
94+
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.
95+
96+
### Step 2: Create a UI to show social login buttons and handle login
97+
98+
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).
99+
100+
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.
101+
102+
The exact implementation of the above can be found [here](/docs/thirdparty/custom-ui/thirdparty-login). The two big differences in the implementation are:
103+
- 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.
104+
- 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`).
105+
106+
### Step 3: Extract the social login access token and user peofile info on the backend
107+
108+
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.
109+
110+
To fetch the new user object and also the third party profile, you can override the signinup recipe function:
111+
112+
<BackendSDKTabs>
113+
<TabItem value="nodejs">
114+
115+
```tsx
116+
import SuperTokens, { User } from "supertokens-node";
117+
import ThirdParty from "supertokens-node/recipe/thirdparty";
118+
import Session from "supertokens-node/recipe/session";
119+
120+
SuperTokens.init({
121+
appInfo: {
122+
apiDomain: "...",
123+
appName: "...",
124+
websiteDomain: "..."
125+
},
126+
supertokens: {
127+
connectionURI: "...",
128+
},
129+
recipeList: [
130+
ThirdParty.init({
131+
// highlight-start
132+
override: {
133+
functions: (originalImplementation) => {
134+
return {
135+
...originalImplementation,
136+
// override the thirdparty sign in / up function
137+
signInUp: async function (input) {
138+
139+
let existingUser: User | undefined = undefined;
140+
if (input.session !== undefined) {
141+
existingUser = await SuperTokens.getUser(input.session.getUserId());
142+
}
143+
144+
let response = await originalImplementation.signInUp(input);
145+
146+
if (response.status === "OK") {
147+
148+
let accessToken = response.oAuthTokens["access_token"];
149+
150+
let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"];
151+
152+
if (input.session !== undefined && response.user.id === input.session.getUserId()) {
153+
if (response.user.loginMethods.length === existingUser!.loginMethods.length + 1) {
154+
// new social account was linked to session user
155+
} else {
156+
// social account was already linked to the session
157+
// user from before
158+
}
159+
}
160+
}
161+
162+
return response;
163+
}
164+
}
165+
}
166+
}
167+
// highlight-end
168+
}),
169+
Session.init({ /* ... */ })
170+
]
171+
});
172+
```
173+
174+
</TabItem>
175+
<TabItem value="go">
176+
177+
:::note
178+
Coming Soon
179+
:::
180+
181+
</TabItem>
182+
<TabItem value="python">
183+
184+
:::note
185+
Coming Soon
186+
:::
187+
188+
</TabItem>
189+
</BackendSDKTabs>
190+
191+
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.

v2/emailpassword/common-customizations/change-password.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ app.post("/change-password", verifySession(), async (req: SessionRequest, res: e
137137
const email = loginMethod.email!;
138138

139139
// call signin to check that input password is correct
140-
let isPasswordValid = await ^{recipeNameCapitalLetters}.^{nodeSignIn}(session!.getTenantId(), email, oldPassword)
140+
let isPasswordValid = await ^{recipeNameCapitalLetters}.^{nodeVerifyCredentials}(session!.getTenantId(), email, oldPassword)
141141

142142
if (isPasswordValid.status !== "OK") {
143143
// TODO: handle incorrect password error

0 commit comments

Comments
 (0)