Implementing account deduplication
The approach to implementing account deduplication is to override the backend functions / APIs which create a user such that:
- We check if a user already exists with that email.
- If a user does not exist, we call the original implementation; Else
- We return a message to the frontend telling the user why sign up was rejected.
Add the following override logic to ThirdPartyPasswordless.init
on the backend
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
import supertokens from "supertokens-node";
ThirdPartyPasswordless.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
thirdPartySignInUp: async function (input) {
let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, {
email: input.email
});
if (existingUsers.length === 0) {
// this means this email is new so we allow sign up
return originalImplementation.thirdPartySignInUp(input);
}
if (existingUsers.find(u =>
u.loginMethods.find(lM => lM.hasSameThirdPartyInfoAs({
id: input.thirdPartyId,
userId: input.thirdPartyUserId
}) && lM.recipeId === "thirdparty") !== undefined)) {
// this means we are trying to sign in with the same social login. So we allow it
return originalImplementation.thirdPartySignInUp(input);
}
// this means that the email already exists with another social or passwordless login method, so we throw an error.
throw new Error("Cannot sign up as email already exists");
}
}
},
apis: (originalImplementation) => {
return {
...originalImplementation,
createCodePOST: async function (input) {
if ("email" in input) {
let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, {
email: input.email
});
if (existingUsers.length === 0) {
// this means this email is new so we allow sign up
return originalImplementation.createCodePOST!(input);
}
if (existingUsers.find(u =>
u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined)) {
// this means that the existing user is a passwordless login user. So we allow it
return originalImplementation.createCodePOST!(input);
}
return {
status: "GENERAL_ERROR",
message: "Seems like you already have an account with another method. Please use that instead."
}
}
// phone number based login, so we allow it.
return originalImplementation.createCodePOST!(input);
},
thirdPartySignInUpPOST: async function (input) {
try {
return await originalImplementation.thirdPartySignInUpPOST!(input);
} catch (err: any) {
if (err.message === "Cannot sign up as email already exists") {
// this error was thrown from our function override above.
// so we send a useful message to the user
return {
status: "GENERAL_ERROR",
message: "Seems like you already have an account with another method. Please use that instead."
}
}
throw err;
}
}
}
}
}
})
import (
"errors"
"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
"github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels"
"github.com/supertokens/supertokens-golang/recipe/thirdpartypasswordless"
"github.com/supertokens/supertokens-golang/recipe/thirdpartypasswordless/tplmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
thirdpartypasswordless.Init(tplmodels.TypeInput{
Override: &tplmodels.OverrideStruct{
Functions: func(originalImplementation tplmodels.RecipeInterface) tplmodels.RecipeInterface {
ogThirdPartySignInUp := *originalImplementation.ThirdPartySignInUp
(*originalImplementation.ThirdPartySignInUp) = func(thirdPartyID string, thirdPartyUserID string, email string, oAuthTokens map[string]interface{}, rawUserInfoFromProvider tpmodels.TypeRawUserInfoFromProvider, tenantId string, userContext *map[string]interface{}) (tplmodels.ThirdPartySignInUp, error) {
existingUsers, err := thirdpartypasswordless.GetUsersByEmail(tenantId, email)
if err != nil {
return tplmodels.ThirdPartySignInUp{}, err
}
if len(existingUsers) == 0 {
// this means this email is new so we allow sign up
return ogThirdPartySignInUp(thirdPartyID, thirdPartyUserID, email, oAuthTokens, rawUserInfoFromProvider, tenantId, userContext)
}
isSignIn := false
for _, user := range existingUsers {
if user.ThirdParty != nil && user.ThirdParty.ID == thirdPartyID && user.ThirdParty.UserID == thirdPartyUserID {
// this means we are trying to sign in with the same social login. So we allow it
isSignIn = true
}
}
if isSignIn {
return ogThirdPartySignInUp(thirdPartyID, thirdPartyUserID, email, oAuthTokens, rawUserInfoFromProvider, tenantId, userContext)
}
return tplmodels.ThirdPartySignInUp{}, errors.New("Cannot sign up as email already exists")
}
return originalImplementation
},
APIs: func(originalImplementation tplmodels.APIInterface) tplmodels.APIInterface {
originalSignInUpPOST := *originalImplementation.ThirdPartySignInUpPOST
originalCreateCodePOST := *originalImplementation.CreateCodePOST
(*originalImplementation.CreateCodePOST) = func(email, phoneNumber *string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) {
if email != nil {
existingUsers, err := thirdpartypasswordless.GetUsersByEmail(tenantId, *email)
if err != nil {
return plessmodels.CreateCodePOSTResponse{}, err
}
isSignIn := false
for _, user := range existingUsers {
if user.ThirdParty == nil {
// this means that the existing user is a passwordless login user. So we allow it
isSignIn = true
}
}
if isSignIn {
return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext)
}
return plessmodels.CreateCodePOSTResponse{
GeneralError: &supertokens.GeneralErrorResponse{
Message: "Seems like you already have an account with another method. Please use that instead.",
},
}, nil
}
// phone number based login, so we allow it.
return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext)
}
(*originalImplementation.ThirdPartySignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext *map[string]interface{}) (tplmodels.ThirdPartySignInUpPOSTResponse, error) {
resp, err := originalSignInUpPOST(provider, input, tenantId, options, userContext)
if err.Error() == "Cannot sign up as email already exists" {
// this error was thrown from our function override above.
// so we send a useful message to the user
return tplmodels.ThirdPartySignInUpPOSTResponse{
GeneralError: &supertokens.GeneralErrorResponse{
Message: "Seems like you already have an account with another method. Please use that instead.",
},
}, nil
}
return resp, err
}
return originalImplementation
},
},
})
}
from supertokens_python import init, InputAppInfo
from supertokens_python.types import GeneralErrorResponse
from supertokens_python.recipe import thirdpartypasswordless
from supertokens_python.recipe.thirdpartypasswordless.asyncio import get_users_by_email
from supertokens_python.recipe.thirdpartypasswordless.interfaces import APIInterface, RecipeInterface, ThirdPartySignInUpOkResult, ThirdPartyAPIOptions, PasswordlessAPIOptions
from typing import Union, Dict, Any
from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo
from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider
def override_thirdpartypasswordless_functions(original_implementation: RecipeInterface):
original_thirdparty_sign_in_up = original_implementation.thirdparty_sign_in_up
async def thirdparty_sign_in_up(
third_party_id: str,
third_party_user_id: str,
email: str,
oauth_tokens: Dict[str, Any],
raw_user_info_from_provider: RawUserInfoFromProvider,
tenant_id: str,
user_context: Dict[str, Any]
) -> ThirdPartySignInUpOkResult:
existing_users = await get_users_by_email(tenant_id, email, user_context)
if (len(existing_users) == 0):
# this means this email is new so we allow sign up
return await original_thirdparty_sign_in_up(third_party_id, third_party_user_id, email, oauth_tokens, raw_user_info_from_provider, tenant_id, user_context)
if (len([x for x in existing_users if
x.third_party_info is not None and x.third_party_info.id == third_party_id and
x.third_party_info.user_id == third_party_user_id]) > 0):
# this means we are trying to sign in with the same social login. So we allow it
return await original_thirdparty_sign_in_up(third_party_id, third_party_user_id, email, oauth_tokens, raw_user_info_from_provider, tenant_id, user_context)
# this means that the email already exists with another social or passwordless login method.
# so we throw an error.
raise Exception("Cannot sign up as email already exists")
original_implementation.thirdparty_sign_in_up = thirdparty_sign_in_up
return original_implementation
def override_thirdpartypasswordless_apis(original_implementation: APIInterface):
original_sign_in_up_post = original_implementation.thirdparty_sign_in_up_post
original_create_code_post = original_implementation.create_code_post
async def create_code_post(email: Union[str, None], phone_number: Union[str, None], tenant_id: str, api_options: PasswordlessAPIOptions,
user_context: Dict[str, Any],
):
if email is not None:
existing_users = await get_users_by_email(tenant_id, email, user_context)
if (len(existing_users) == 0):
# this means this email is new so we allow sign up
return await original_create_code_post(email, phone_number, tenant_id, api_options, user_context)
if (len([x for x in existing_users if x.third_party_info is None]) > 0):
# this means that the existing user is a passwordless login user. So we allow it
return await original_create_code_post(email, phone_number, tenant_id, api_options, user_context)
return GeneralErrorResponse("Seems like you already have an account with another social login provider. Please use that instead.")
# phone number based login, so we allow it.
return await original_create_code_post(email, phone_number, tenant_id, api_options, user_context)
async def sign_in_up_post(
provider: Provider,
redirect_uri_info: Union[RedirectUriInfo, None],
oauth_tokens: Union[Dict[str, Any], None],
tenant_id: str,
api_options: ThirdPartyAPIOptions,
user_context: Dict[str, Any]
):
try:
return await original_sign_in_up_post(provider, redirect_uri_info, oauth_tokens, tenant_id, api_options, user_context)
except Exception as e:
if str(e) == "Cannot sign up as email already exists":
return GeneralErrorResponse("Seems like you already have an account with another method. Please use that instead.")
raise e
original_implementation.thirdparty_sign_in_up_post = sign_in_up_post
original_implementation.create_code_post = create_code_post
return original_implementation
init(
app_info=InputAppInfo(
api_domain="...", app_name="...", website_domain="..."),
framework='...',
recipe_list=[
thirdpartypasswordless.init(
contact_config=...,
flow_type="...",
override=thirdpartypasswordless.InputOverrideConfig(
apis=override_thirdpartypasswordless_apis,
functions=override_thirdpartypasswordless_functions
),
)
]
)
In the above code snippet, we override the thirdPartySignInUpPOST
and the createCodePOST
API as well as the thirdPartySignInUp
recipe function.
The thirdPartySignInUpPOST
API is called by the frontend after the user is redirected back to your app from the third party provider's login page. The API then exchanges the auth code with the provider and calls the signInUp
function with the user's email and thirdParty info.
The createCodePOST
API is called when the user enters their email, or phone number during passwordless login. This API generates the passwordless OTP / link and sends it to the user's email / phone.
We override the thirdPartySignInUp
recipe function to:
- Get all ThirdParty or Passwordless users that have the same input email.
- If no users exist with that email, it means that this is a new email and so we call the
originalImplementation
function which creates a new user. - If instead, a user exists, but has the same
thirdPartyId
andthirdPartyUserId
, implying that this is a sign in (for example a user who had signed up with Google is signing in with Google), we again allow the operation by calling theoriginalImplementation
function. - If neither of the conditions above match, it means that the user is trying to sign up with a third party provider whilst they already have an account with another provider or via passwordless login. So here we throw an error with some custom message.
Finally, we override the signInUpPOST
API to catch that custom error and return a general error status to the frontend with a message that will be displayed to the user in the sign in form.
We also override the createCodePOST
API to perform similar checks:
- If the input is phone number based, then we call the
originalImplementation
function allowing sign up or sign in. This is OK since social login is always email based, so there is no scope of duplication. - Otherwise, we det all ThirdParty or Passwordless users that have the same input email.
- If no users exist with that email, it means that this is a new email and so we call the
originalImplementation
function which creates a new user. - Else we check if the existing user is not a Third Party login user, implying that it's a Passwordless login user. So here, we also call the
originalImplementation
function to allow the user to sign in. - If neither of the conditions above match, it means that the user is trying to sign up with passwordless login whilst they already have an account with a third party provider. So here we return an appropriate message to be displayed on the frontend.
Multi Tenancy
For a multi tenant setup, the customisations above ensure that multiple accounts with the same email don't exist within a single tenant. If you want to ensure that there is no duplication across all tenants, then when fetching the list of existing users, you should loop through all tenants in your app, which you can fetch by using the listAllTenants
function of the multi tenancy recipe.