Skip to main content
Paid Feature

This is a paid feature.

For self hosted users, Sign up 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, and follow the steps from there on. Once enabled, this feature is free on the provided development environment.

Manual account linking

Manual account linking allows you to take control of when and which accounts are linked. With this, you can implement flows like:

  • Connecting social login accounts to an existing account post login.
  • Adding a password to an account that was created with a social or passwordless login.
  • Linking accounts which don't have the same email or phone number, or have a different identifier alltogether.

Initialilse the account linking recipe#

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: [
AccountLinking.init({
shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => {
return {
shouldAutomaticallyLink: false
}
}
})
]
});

In the above, we tell SuperTokens not to do automatic account linking (during sign up or sign in APIs) by returning shouldAutomaticallyLink: false. Initialising the recipe is still important though so that you can use the functions from our SDK as shown below.

It is of course possible to enable auto account linking and still use the functions for manual account linking below.

Creating a primary user#

In order to link two accounts, you first need to make one of them a primary user:

import AccountLinking from "supertokens-node/recipe/accountlinking";
import {RecipeUserId} from "supertokens-node";

async function makeUserPrimary(recipeUserId: RecipeUserId) {
let response = await AccountLinking.createPrimaryUser(recipeUserId);
if (response.status === "OK") {
if (response.wasAlreadyAPrimaryUser) {
// The input user was already a primary user and accounts can be linked to it.
} else {
// User is now primary and accounts can be linked to it.
}
let modifiedUser = response.user;
console.log(modifiedUser.isPrimaryUser); // will print true
} else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") {
// This happens if there already exists another primary user with the same email or phone number
// in at least one of the tenants that this user belongs to.
} else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") {
// This happens if this user is already linked to another primary user.
}
}

Linking accounts#

Once a user has become a primary user, you can link other accounts to this user:

import AccountLinking from "supertokens-node/recipe/accountlinking";
import { RecipeUserId } from "supertokens-node";

// we are linking the input recipeUserId to the primaryUserId
async function linkAccounts(primaryUserId: string, recipeUserId: RecipeUserId) {
let response = await AccountLinking.linkAccounts(recipeUserId, primaryUserId);
if (response.status === "OK") {
if (response.accountsAlreadyLinked) {
// The input users were already linked
} else {
// The two users are now linked
}
let modifiedUser = response.user;
console.log(modifiedUser.loginMethods); // this will now contain the login method of the recipeUserId as well.
} else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") {
// This happens if there already exists another primary user with the same email or phone number
// as the recipeUserId's account.
} else if (response.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") {
// This happens if the input primaryUserId is not actually a primary user ID.
// You can call createPrimaryUserId and call linkAccountsAgain
} else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") {
// This happens if the input recipe user ID is already linked to another primary user.
// You can call unlink accounts on the recipe user ID and then try linking again.
}
}

Unlinking accounts#

If you want to unlink an account from its primary user ID, you can use the following function:

import AccountLinking from "supertokens-node/recipe/accountlinking";
import { RecipeUserId } from "supertokens-node";

async function unlinkAccount(recipeUserId: RecipeUserId) {
let response = await AccountLinking.unlinkAccount(recipeUserId);
if (response.status === "OK") {
if (response.wasLinked) {
// This means that we unlinked the account from its primary user ID
} else {
// This means that the user was never linked in the first place
}

if (response.wasRecipeUserDeleted) {
// This is true if we call unlinkAccount on the recipe user ID of the primary user ID user.
// We delete this user because if we don't and we call getUserById() on this user's ID, SuperTokens
// won't know which user info to return - the primary user, or the recipe user.
// Note that even though the recipe user is deleted, the session, metadata, roles etc for this
// primary user is still intact, and calling getUserById(primaryUserId) will still return
// the user object with the other login methods.
} else {
// There not exists a user account which is not a primary user, with the recipeUserId = to the
// input recipeUserId.
}

}
}

Converting a string userId into a recipeUserId#

If you notice, the input to a lot of the functions above is of type RecipeUserId. You can convert a string userID into a RecipeUserId in the following way:

import SuperTokens from "supertokens-node";

async function getAsRecipeUserIdType(userId: string) {
return SuperTokens.convertToRecipeUserId(userId);
}

The reason we have this type is because it prevents bugs wherein a function is expecting a recipe user id (like createNewSession, or updateEmailOrPassword from email password recipe), but you pass in the primary user ID instead.

Other helper functions#

Our SDK also exposes several other helper functions:

  • AccountLinking.createPrimaryUserIdOrLinkAccounts: Given a recipe user ID, this function will attempt linking it with any primary user ID that has the same email or phone number associated with it. If no such primary user exists, this function will make the input user account a primary one.

  • AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId: Given a recipe user ID, this function will return a primary user ID which this user can be linked to, based on matching emails / phone numbers. If no such primary user exists, this function will return undefined.

  • AccountLinking.canCreatePrimaryUser: Given a recipe user ID, this function will return a status OK if the user can be made a primary user, and a different status otherwise (indicating why it can't become a primary user). A user can be made a primary user if there exists no other primary user with the same email or phone number across all the tenants that this user belongs to.

  • AccountLinkling.canLinkAccounts: Given a recipeUserId and a primary user ID, this function returns a status OK if the accounts can be linked, and if not, it returns a different status (indicating why the accounts can't be linked). Accounts can be linked if the recipe user ID is not already linked to another primary user, and if the resulting primary user does not have any email / phone number in common with another primary user across all of the tenants that it belongs to.

  • AccountLinking.isSignUpAllowed: Given the login info (email for example) of the new user, who is trying to sign up, this function returns true if it's safe to allow them to sign up, false otherwise. See the error codes in the automatic account linking page to see why this might return false.

  • AccountLinking.isSignInAllowed: Given the login info (email for example) of a user, who is trying to sign in, this function returns true if it's safe to allow them to sign in, false otherwise. See the error codes in the automatic account linking page to see why this might return false.

  • AccountLinking.isEmailChangeAllowed: Given the recipe user id and the new email for update, this function returns true if it's safe to update the email, else false. Below are the conditions in which false is returned:

    • If the input recipe user is a primary user, then we need to check that the new email doesn't belong to any other primary user. If it does, we disallow the change since multiple primary user's can't have the same email.
    • If the recipe user is NOT a primary user, and if the new email is not verified, then we check if there exists a primary user with the same email, and if it exists, we disallow the email change. We disallow because if this email is changed, and an email verification email is sent, then the primary user may end up clicking on the link by mistake, causing account linking to happen which can result in account take over if this recipe user is malicious.