Add passwords to an existing account
Overview
There may be scenarios in which you want to add a password to an account created using a social provider or passwordless login. This guide walks you through how to do this.
The idea here is to reuse the existing sign up APIs, but call them with a session's access token. The APIs 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 this guide goes through them as well.
Before you start
This feature is only available to paid users.
We do not provide pre-built UI for this flow since it's probably something you want to add in your settings page or during the sign up process. This guide focuses 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 the supertokens-auth-react
SDK, on the frontend.
Steps
1. Enable account linking and emailpassword
on the backend SDK
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: [
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
}
}
})
]
});
In the above implementation of shouldDoAutomaticAccountLinking
, account linking is only allowed if the input session is present. This means that the system links an email password account to an existing session user. Otherwise, account linking is not allowed, which means that first factor account linking is not enabled. If you want to enable that too, you can see the automatic account linking documentation.
2. Create a UI to show a password input to the user and handle the submit event
If you want to use password based auth as a second factor, or for step up auth, see the docs in the MFA recipe 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 need to detect if there already exists a password for the user. You can do this by inspecting the user object on the backend and checking if there is an emailpassword
login method.
Then, if no such login method exists, you have to show a UI in which the user can add a password to their account. The password validation documentation contains the default password validation rules.
You also need to fetch the email of the user before you call the email password sign up API. You can fetch this using 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 the passwordless recipe. This step should occur before asking them to set a password. The email OTP flow also results in creating a passwordless user account and linking it to the session user.
Once you have the email on the frontend, you should call the sign up API. The two big differences in the implementation are:
- When you call the sign up API, you need to provide the session's access token in the request. If you are using the frontend SDK, this process happens automatically via the frontend network interceptors. The access token enables the backend to get a session and then link the email password account to session user.
- New types of failure scenarios exist when calling the sign up API which are impossible during first factor login. To learn more about them, see the error codes section (>
ERR_CODE_008
).
3. Check for email match in the backend sign up API
Since the frontend specifies the email, verify it in the backend API before using it (since the frontend shouldn't be trusted). You can do this by overriding the email password sign up API:
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({
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);
}
}
}
}
}),
Session.init({ /* ... */ })
]
});