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.
Automatic account linking
Automatic account linking is a feature that allows users to automatically sign in to their existing account using more than one login method. On a high level, the accounts for the different login methods are linked automatically by SuperTokens provided that:
- Their emails or phone numbers are the same.
- Their emails or phone numbers are verified.
SuperTokens ensures that accounts are automatically linked only if there is no risk of account takeover.
#
Enabling automatic account linkingYou can enable this feature by providing the following callback implementation on the backend SDK:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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) => {
if (session !== undefined) {
return {
shouldAutomaticallyLink: false
}
}
if (newAccountInfo.recipeUserId !== undefined && user !== undefined) {
let userId = newAccountInfo.recipeUserId.getAsString();
let hasInfoAssociatedWithUserId = false // TODO: add your own implementation here.
if (hasInfoAssociatedWithUserId) {
return {
shouldAutomaticallyLink: false
}
}
}
return {
shouldAutomaticallyLink: true,
shouldRequireVerification: true
}
}
})
]
});
note
Coming Soon
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import accountlinking
from supertokens_python.types import User
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.recipe.accountlinking.types import AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink
from typing import Dict, Any, Optional, Union
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 session is not None:
return ShouldNotAutomaticallyLink()
if new_account_info.recipe_user_id is not None and user is not None:
_user_id = new_account_info.recipe_user_id.get_as_string()
has_info_associated_with_user_id = False # TODO: add your own implementation here.
if has_info_associated_with_user_id:
return ShouldNotAutomaticallyLink()
return ShouldAutomaticallyLink(should_require_verification=True)
init(
app_info=InputAppInfo(
app_name="...",
api_domain="...",
website_domain="...",
),
supertokens_config=SupertokensConfig(
connection_uri="...",
),
framework='...',
recipe_list=[
accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking)
],
)
#
Input args meaning:newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }
: This object contains information about the user whose account is going to be linked, or will become a primary user. The object contains the user's email, social login info and phone number (whichever they used to sign in / up with). It also contains the login method (emailpassword
,thirdparty
, orpasswordless
). It may also contain therecipeUserId
of the user that is going to be linked in case SuperTokens is attempting account linking during sign in.Notice that in case of
newAccountInfo.recipeUserId !== undefined && user !== undefined
beingtrue
, we add some extra logic to check if the user ID has any info associated with them in your application db. This is to prevent data loss for this user ID (see the migtation section below).user: User | undefined
: If this is notundefined
, it means thatnewAccountInfo
user is about to linked to thisuser
. If this isundefined
, it means thatnewAccountInfo
user is about to become a primary user.session: SessionContainerInterface | undefined
: The session object of the user that is about to be linked. This isundefined
if it's the first factor login. If a user has completed the first factor, and calls a sign up / in API again of some login method (either for MFA or for social login linking), then thesession
object will be defined. In the above snippet, we prevent account linking if a session is defined so that account linking only happens during the first factor login.tenant: string
: The ID of the tenant that the user is signing in / up to.userContext: any
: User defined userContext.
#
Output args meaning:shouldAutomaticallyLink
: If this istrue
, it means that thenewAccountInfo
will be linked or will become a primary user during this API call (assuming a set of security checks pass). If this isfalse
, it means that there will be no account linking related operation during this API call.shouldRequireVerification
: If this istrue
, that account linking operations will only happen if thenewAccountInfo
is verified. We strongly recommend keeping it set totrue
for security reasons.
important
If you are returning shouldRequireVerification
as true
, then you need to also enable the email verification recipe in REQUIRED
mode. This means that if the login method does not inherently verify the email (like for email password login), SuperTokens will require the user to go through the email verification flow first and then attempt auto linking of the account. For other login methods like sign in with Google, the email is already verified during login, so the user will not be asked to verify the email again, and account linking will be tried immediately.
If you enable email verification in OPTIONAL
mode, then whilst the user can access the account after email password login, account linking will only be attempted after they have verified their email later on. This is risky because whilst the user had access to their email password account after sign up, they could loose access to that after verification and after account linking has been completed due to the change in the primary user ID (though we provide a callback - see below - to help you migrate data from one user ID to another).
You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not.
#
Different scenarios of automatic account linkingDuring sign up: If there exists another account with the same email or phone number within the current tenant, the new account will be linked to the existing account if:
- The existing account is a primary user
- If
shouldRequireVerification
istrue
, the new account needs to be created via a method that has the email as verified (for example via passwordless or google login). If the new method doesn't inherently verify the email (like in email password login), the the accounts will be linked post email verification. - Your implementation for
shouldDoAutomaticAccountLinking
returnstrue
for theshouldAutomaticallyLink
boolean.
During sign in: If the current user is not already linked and if there exists another user with the same email or phone number within the current tenant, the accounts will be linked if:
- The user being signed into is NOT a primary user, and the other user with the same email / phone number IS a primary user
- If
shouldRequireVerification
istrue
, the current account (that's being signed into) has its email as verified. - Your implementation for
shouldDoAutomaticAccountLinking
returnstrue
for theshouldAutomaticallyLink
boolean.
After email verification: If the current user whose email got verified is not a primary user, and there exists another primary user in the same tenant with the same email, then we link the two accounts if:
- Your implementation for
shouldDoAutomaticAccountLinking
returnstrue
for theshouldAutomaticallyLink
boolean.
- Your implementation for
During password reset flow: If there already exists a user with the same email in a non email password recipe (social login for example), and the user is doing a password reset flow, a new email password user is created and linked to the existing account if:
- The non email password user is a primary user.
- Your implementation for
shouldDoAutomaticAccountLinking
returnstrue
for theshouldAutomaticallyLink
boolean.
#
Affect on email verificationFor a primary user, if there exists two login method (L1 & L2), and they both have the same email, but the email for L1 is verified and not for L2, SupeTokens will auto verify the email for L2 when:
- The user next logs in with L2.
- If you call
updateEmailOrPassword
from the email password orupdateUser
from the passwordless recipe, updating L2's email to be equal to L1's email.
#
Affect on email updateWhen updating the email of a login method for a user, SuperTokens needs to make sure that the account linking conditions mentioned above remain intact. This means that you cannot update the email of a primary user to a value that matches the email of another primary user.
For example, if User A has login method AL1
(email e1
) and AL2
(email e1
), and User B has login method BL1
(email e2
) and BL2
(email e3
), then we cannot update AL1
email to e2
or e3
because that would lead to two primary users having the same email.
Now email updates can happen in different scenarios:
- 1) Calling the
updateEmailOrPassword
from the email password recipe - 2) Calling the
updateUser
function from the passwordless recipe - 3) Logging in via social login can also update emails if the email has changed from the provider's side.
In each of these cases, the operation will fail and an appropriate status code will be returned:
- For function calls (1) and (2), you will get back a response with a status indicating that email update was not possible.
- For social login API call (3), the client will get a response with a status indicating to contact support, with a support status code (see below).
#
Migration of user data when accounts are linkedWhen two accounts are linked the primary user ID of the non primary user changes.
For example, if we have User A with with primary user ID p1
and user B, which is a non primary user, and has a user ID of p2
, and we link them, then the primary user ID of User B will be changed to p1
.
This has an effect that if the user logs in with login method from User B, the session.getUserId()
will return p1
. If there was any older data associated with User B (against user ID p2
), in your database, that data will essentially be "lost".
To prevent this scenario, you should:
Make sure that you return
false
forshouldAutomaticallyLink
boolean in theshouldDoAutomaticAccountLinking
function implementation if there exists arecipeUserId
in thenewAccountInfo
object, and if you have some information related to that user ID in your own database. This can be seen in the code snippet above.If you do not want to return
false
in this case, and want the accounts to be linked, then make sure to implement theonAccountLinked
callback:import supertokens, { User, RecipeUserId } from "supertokens-node";
import AccountLinking from "supertokens-node/recipe/accountlinking";
import { AccountInfoWithRecipeId, RecipeLevelUser } 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: true,
shouldRequireVerification: true
}
},
onAccountLinked: async (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => {
let olderUserId = newAccountInfo.recipeUserId.getAsString()
let newUserId = user.id;
// TODO: migrate data from olderUserId to newUserId in your database...
}
})
]
});
caution
If your logic in onAccountLinked
throws an error, then it will not be called again, and will still have resulted in the accounts being linked. However, the end user would see an error on the UI as the API would return a 500
status code. They can retry the login action and will be logged into the primary user's account as expected.
#
Support status codesThe following is a list of support status codes that the end user might see during their interaction with the login UI (as a general error message in the pre built UI).
#
ERR_CODE_001This can happen during creating a password reset code in the email password flow:
- API path and method:
/user/password/reset/token POST
- Output JSON:
{
"status": "PASSWORD_RESET_NOT_ALLOWED",
"reason":
"Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)"
} - The pre build UI on the frontend displays this error in the following way:
- API path and method:
Below is the scenario for when this status is returned:
A malicious user, User A, which is a primary user, has login methods with email
e1
(social login) and emaile1
(email password login). If user A changes their emailpassword email toe2
(which is now in unverified state), and the real user ofe2
(the victim) tries to sign up via email password, they will see a message saying that the email already exists. The victim may then try to do a password reset (thinking they had previously signed up). If we allow this to happen, and the victim resets the password (since they are the real owner of the email), then they will be able to login to the account, and the attacker can spy on what the user is doing via their third party login method.To prevent this scenario, we enforce that the password link is only generated if the primary user has at least one login method that has the input email ID and is verified, or if not, we check that the primary user has no other login method with a different email, or phone number. If these cases are not satisfied, then we return the error code
ERR_CODE_001
.To resolve this, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. You can do these actions using our user management dashboard.
#
ERR_CODE_002This can happen during the passwordless recipe's create or consume code API (during sign up):
- API path and method:
/signinup/code POST
or/signinup/code/consume POST
- Output JSON:
{
"status": "SIGN_IN_UP_NOT_ALLOWED",
"reason": "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_002)"
} - The pre build UI on the frontend displays this error in the following way:
- API path and method:
Below is as example scenario for when this status is returned (one amongst many): A user is trying to sign up using passwordless login method with email
e1
. There exists an email password login method withe1
, which is unverified (owned by an attacker). If we allow the passwordless sign up, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they just signed up, they do not get suspicious), and then the attacker's login method is linked to the passwordless login method. This way, the attacker now has access to the user's account.To prevent this, we do not allow sign up with passwordless login in case there exists another account with the same email and is unverified.
To resolve this issue, you should ask the user to try another login method (which already has their email), or then mark their email as verified in the other account that has the same email, before asking them to retry passwordless login. You can do these actions using our user management dashboard.
#
ERR_CODE_003This used to be an error code which is no longer valid and can be ignored.
#
ERR_CODE_004This can happen during the thirdparty recipe's signinup API (during sign in):
- API path and method:
/signinup POST
- Output JSON:
{
"status": "SIGN_IN_UP_NOT_ALLOWED",
"reason": "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_004)"
} - The pre build UI on the frontend displays this error in the following way:
- API path and method:
Below is as example scenario for when this status is returned (one amongst many): There exists a thirdparty user with email
e1
, sign in with Google (owned by the victim, and the email is verified). There exists another third party login method with email,e2
(owned by an attacker), let's say it's login with Github. The attacker then goes to their Github and changes their email toe1
(which is in unverified state). The next time the attacker tries to login, via github, they will see this error code. We prevent login, because if we didn't, then the attacker might send an email verification link toe1
, and if the victim clicks on it, then the attacker's account will be linked to the victim's account.To resolve this issue, you can delete the login method that has the unverified email, or if manually mark the unverified account as verified (if you confirm the identity of its owner). You can do these actions using our user management dashboard.
#
ERR_CODE_005This can happen during the thirdparty recipe's signinup API (during sign in):
- API path and method:
/signinup POST
- Output JSON:
{
"status": "SIGN_IN_UP_NOT_ALLOWED",
"reason": "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_005)"
} - The pre build UI on the frontend displays this error in the following way:
- API path and method:
Below is as example scenario for when this status is returned (one amongst many): There exists a primary, thirdparty user with email
e1
, sign in with Google. There exists another email password user with emaile2
, which is a primary user. Now, if the user changes their email on Google toe2
, and then try logging in via Google, they will see this error code. We do this because if we didn't, then it would result in two primary users having the same email, which voilates one of the account linking rules.To resolve this issue, you can make one of the primary users as non primary (this can be done by using the unlink button against the login methon on our user management dashboard). Once the user is not a primary user, you can ask the user to relogin with that method, and it should auto link that account with the existing primary user.
#
ERR_CODE_006This can happen during the thirdparty recipe's signinup API (during sign up):
- API path and method:
/signinup POST
- Output JSON:
{
"status": "SIGN_IN_UP_NOT_ALLOWED",
"reason": "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_006)"
} - The pre build UI on the frontend displays this error in the following way:
- API path and method:
Below is as example scenario for when this status is returned (one amongst many): A user is trying to sign up using third party login method with email
e1
. There exists an email password login method withe1
, which is unverified (owned by an attacker). If we allow the third party sign up, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they just signed up, they do not get suspicious), and then the attacker's login method is linked to the third party login method. This way, the attacker now has access to the user's account.To prevent this, we do not allow sign up with third party login in case there exists another account with the same email and is unverified.
To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. You can do these actions using our user management dashboard.
#
ERR_CODE_007This can happen during the email password signup API:
- API path and method:
/signup POST
- Output JSON:
{
"status": "SIGN_UP_NOT_ALLOWED",
"reason": "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)"
} - The pre build UI on the frontend displays this error in the following way:
- API path and method:
Below is as example scenario for when this status is returned (one amongst many): There exists a primary, social login account with email
e1
, sign in with Google. Now an attacker tries to sign up with email password with emaile1
. If we allow this, an email verificaiton email will be sent to the victim, and they may click it since they had previously signed up with Google. This will result in the attacker's account being linked to the victim's account.To resolve this issue, you can ask the user to try and login, or go through the reset password flow.
#
ERR_CODE_008This can happen during the email password signin API:
- API path and method:
/signin POST
- Output JSON:
{
"status": "SIGN_IN_NOT_ALLOWED",
"reason": "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)"
} - The pre build UI on the frontend displays this error in the following way:
- API path and method:
Below is as example scenario for when this status is returned (one amongst many): There exists a primary, social login account with email
e1
, sign in with Google. There also exists an email password account (owned by the attacker) that is unverified with the same emaile1
(this is not a primary user). Now if the attacker tries to sign in with email password, they will see this error. We do this because if we didn't, then the attacker might send an email verification email on sign in, and the actual user may click on it (since they had previously signed up). Upon verifying that account, the attacker's account will be linked to the victim's account.To resolve this issue, you can ask the user to try the reset password flow.
#
ERR_CODE_014This can happen when adding a password to an existing session user:
- API Path is
/signup POST
. - Output JSON:
{
"status": "SIGN_UP_NOT_ALLOWED",
"reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)"
}
- API Path is
An example scenario of when in the following scenario:
- Let's say that the app is confgured to not have autmatic account linking during the first factor.
- A user creates an email password account with email
e1
, verifies it, and links social login account to it with emaile2
. - The user logs out, and then creates a social login account with email
e1
. Then, they are asked to add a password to this account. Since an email password account withe1
already exists, SuperTokens will try and link that to this new account, but fail, since the email password account withe1
is already a primary user.
To resolve this, we recommend that manually link the
e1
social login account with thee1
email password account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen.
#
ERR_CODE_015This can happen when adding a password to an existing session user:
- API Path is
/signup POST
. - Output JSON:
{
"status": "SIGN_UP_NOT_ALLOWED",
"reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)"
}
- API Path is
An example scenario of when in the following scenario:
- A user creates a social login account with email
e1
which becomes a primary user. - The user logs out, and creates another social login account with email
e2
, which also becomes a primary user. - The user is asked to add a password for the new account with an option to also specify an email with it (this is strange, but theoritically possible). They now enter the email
e1
for the email password account. - This will cause this type of error since the linking of the new social logn and email account will fail since there already exists another primary user with the same (
e1
) email.
- A user creates a social login account with email
To resolve this, we recommend not allowing users to specify an email when asking them to add a password for their account.
#
ERR_CODE_016This can happen when adding a password to an existing session user:
- API Path is
/signup POST
. - Output JSON:
{
"status": "SIGN_UP_NOT_ALLOWED",
"reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)"
}
- API Path is
An example scenario of when in the following scenario:
- Let's say that the app is configured to not have automatic account linking during the first factor.
- A user signs up with a social login account using Google with email
e1
, and they add another social account, with Facebook, with the same email. - The user logs out and creates another social login account with email
e1
(say Github), and then tries and adds a password to this account with emaile1
. Here, SuperTokens will try and make the Github login a primary user, but fail, since the emaile1
is already a primary user (with Google login).
To resolve this, we recommend that manually link the
e1
Github social login account with thee1
Google social login account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen.
#
ERR_CODE_020- This can happen during association of a third party login to an existing session's account.
- API Path is
/signinup POST
. - Output JSON:
{
"status": "SIGN_IN_UP_NOT_ALLOWED",
"reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_020)"
}
- API Path is
- This can happen when the thirdparty account that is trying to be linked to the session's account is not verified. It could happen when you are trying to associate a social login account to a user, but that social account's email is not verified (and if the email of that account is not the same as the current session's account's email).
- To resolve this, you can return
shouldRequireVerification
asfalse
in theshouldDoAutomaticAccountLinking
function implementation, or you can only allow users to link social login accounts that give verified accounts.
#
ERR_CODE_021- This can happen during association of a third party login to an existing session's account.
- API Path is
/signinup POST
. - Output JSON:
{
"status": "SIGN_IN_UP_NOT_ALLOWED",
"reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_021)"
}
- API Path is
- This can happen when the thirdparty account that is trying to be linked to the session's account is already linked with another primary user.
#
ERR_CODE_022- This can happen during association of a third party login to an existing session's account.
- API Path is
/signinup POST
. - Output JSON:
{
"status": "SIGN_IN_UP_NOT_ALLOWED",
"reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_022)"
}
- API Path is
- This can happen when the thirdparty account that is trying to be linked to the session's account has the same email as another primary user.
#
ERR_CODE_023- This can happen during association of a third party login to an existing session's account.
- API Path is
/signinup POST
. - Output JSON:
{
"status": "SIGN_IN_UP_NOT_ALLOWED",
"reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_023)"
}
- API Path is
- In order to link the third party user with the session user, we need to make sure that the session user is a primary user. However, that can fail if there exists another primary user with the same email as the session user, and in this case, this error is returned to the frontend.
#
ERR_CODE_024- This happens during third party sign in, when the user is trying to sign in with a non-primary user, and the third party provider does not verify their email, and their exists a primary user with the same email. This can also happen the other way around wherein the user is trying to sign in with the primary user (unverified email), and there exists a non-primary user with the same email.
- API Path is
/signinup POST
. - Output JSON:
{
"status": "SIGN_IN_UP_NOT_ALLOWED",
"reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_024)"
}
- API Path is
- This can be resolved by deleting the (non primary) user that has the same email ID, or by manually marking the email of the user as verified for the login method that they are trying to sign in with.
#
Changing the error message on the frontendIf you want to display a different message to the user, or use a different status code, you can change them on the frontend via the language translation feature.