---
title: Add passwords to an existing account
description: Add a new password to an existing account using the account linking feature.
sidebar:
  order: 7
---

## 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

<PaidFeatureCallout />

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

<DependentContent passive group="backend-language">
<ContentOption title="Go" value="go">
:::note[At the moment this feature is not supported through the Go SDK.]
:::
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" 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 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 && session.getTenantId() === tenantId) {
          return {
            shouldAutomaticallyLink: true,
            shouldRequireVerification: true,
          };
        }
        return {
          shouldAutomaticallyLink: false,
        };
      },
    }),
  ],
});
```
</Tab>
<Tab title="Go" value="go">

</Tab>
<Tab title="Python" value="python">
```python
from typing import Any, Dict, Optional, Union

from supertokens_python.recipe import accountlinking, emailpassword
from supertokens_python.recipe.accountlinking.types import (
    AccountInfoWithRecipeIdAndUserId,
    ShouldAutomaticallyLink,
    ShouldNotAutomaticallyLink,
)
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.types import User


async def should_do_automatic_account_linking(
    new_account_info: AccountInfoWithRecipeIdAndUserId,
    user: Optional[User],
    session: Optional[SessionContainer],
    tenant_id: str,
    user_context: Dict[str, Any],
) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]:
    if user is None:
        return ShouldAutomaticallyLink(should_require_verification=True)

    if (
        session is not None
        and session.get_user_id() == user.id
        and session.get_tenant_id() == tenant_id
    ):
        return ShouldAutomaticallyLink(should_require_verification=True)

    return ShouldNotAutomaticallyLink()


recipe_list = [
    emailpassword.init(),
    accountlinking.init(
        should_do_automatic_account_linking=should_do_automatic_account_linking
    ),
]
```
</Tab>
</CodeGroup>

The callback allows a new user to become a primary user when `user` is absent. It links to an existing user only when
the session user and tenant match the proposed primary user and current tenant. It therefore does not enable linking
between existing users during first-factor authentication. To enable that behavior, see the
[automatic account linking documentation](./automatic-account-linking).

### 2. Create a UI to show a password input to the user and handle the submit event

:::note
If you want to use password based auth as a second factor, or for step up auth, see the docs in the [MFA recipe](/additional-verification/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 need to detect if there already exists a password for the user. You can do this by inspecting the [user object](/references/backend-sdks/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](/authentication/email-password/customize-the-sign-up-form#change-field-validators) contains the default password validation rules.

You also need to fetch a verified email for the current tenant before you call the email-password sign-up API. Fetch it
on the backend from a login method on the user object whose `tenantIds` contains the session tenant. Do not accept an
email from the client as proof of ownership. If no tenant-scoped, verified email exists, first complete an email OTP
flow through the passwordless recipe and link that login method to the same 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](./automatic-account-linking#error-status-codes) (> `ERR_CODE_008`).

### 3. Check for email match in the backend sign up API
Since the frontend specifies the email, verify its ownership on the backend before using it. The email must belong to a
verified login method for the session user in the request tenant. You can enforce this by overriding the email-password
sign-up API:

<DependentContent passive group="backend-language">
<ContentOption title="Go" value="go">
:::note[At the moment this feature is not supported through the Go SDK.]
:::
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```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({
      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((field) => field.id === "email")?.value;
                if (typeof inputEmail !== "string") {
                  return {
                    status: "GENERAL_ERROR",
                    message: "A valid email is required",
                  };
                }
                const sessionUserId = input.session.getUserId();
                const tenantId = input.tenantId;
                if (input.session.getTenantId() !== tenantId) {
                  return {
                    status: "GENERAL_ERROR",
                    message: "Cannot add a password across tenants",
                  };
                }
                const userObject = await SuperTokens.getUser(sessionUserId);
                const ownsVerifiedEmail = userObject?.loginMethods.some(
                  (loginMethod) =>
                    loginMethod.tenantIds.includes(tenantId) &&
                    loginMethod.verified &&
                    loginMethod.hasSameEmailAs(inputEmail),
                );
                if (!ownsVerifiedEmail) {
                  return {
                    status: "GENERAL_ERROR",
                    message: "Cannot use this email to add a password for this user",
                  };
                }
              }
              return await originalImplementation.signUpPOST!(input);
            },
          };
        },
      },
    }),
    Session.init({
      /* ... */
    }),
  ],
});
```
</Tab>
<Tab title="Go" value="go">

</Tab>
<Tab title="Python" value="python">
```python
from typing import Any, Dict, List, Optional, Union

from supertokens_python.asyncio import get_user
from supertokens_python.recipe import emailpassword
from supertokens_python.recipe.emailpassword.interfaces import (
    APIInterface,
    APIOptions,
    EmailAlreadyExistsError,
    SignUpPostNotAllowedResponse,
    SignUpPostOkResult,
)
from supertokens_python.recipe.emailpassword.types import FormField
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.types import GeneralErrorResponse


def override_emailpassword_apis(original_implementation: APIInterface) -> APIInterface:
    original_sign_up_post = original_implementation.sign_up_post

    async def sign_up_post(
        form_fields: List[FormField],
        tenant_id: str,
        session: Optional[SessionContainer],
        should_try_linking_with_session_user: Optional[bool],
        api_options: APIOptions,
        user_context: Dict[str, Any],
    ) -> Union[
        SignUpPostOkResult,
        EmailAlreadyExistsError,
        SignUpPostNotAllowedResponse,
        GeneralErrorResponse,
    ]:
        if session is not None:
            input_email = next(field.value for field in form_fields if field.id == "email")
            user = await get_user(session.get_user_id(), user_context)
            owns_verified_email = user is not None and any(
                tenant_id in login_method.tenant_ids
                and login_method.verified
                and login_method.has_same_email_as(input_email)
                for login_method in user.login_methods
            )
            if session.get_tenant_id() != tenant_id or not owns_verified_email:
                return GeneralErrorResponse(
                    message="Cannot use this email to add a password for this user"
                )

        return await original_sign_up_post(
            form_fields,
            tenant_id,
            session,
            should_try_linking_with_session_user,
            api_options,
            user_context,
        )

    original_implementation.sign_up_post = sign_up_post
    return original_implementation


emailpassword.init(
    override=emailpassword.EmailPasswordOverrideConfig(
        apis=override_emailpassword_apis
    )
)
```
</Tab>
</CodeGroup>

---

## See also

<CardGroup cols={3}>
  <Card title="Automatic account linking" href="/post-authentication/account-linking/automatic-account-linking" />
  <Card title="Link social accounts" href="/post-authentication/account-linking/link-social-accounts" />
  <Card title="Manual account linking" href="/post-authentication/account-linking/manual-account-linking" />
  <Card title="Password hashing" href="/authentication/email-password/password-hashing" />
</CardGroup>
