User Creation without password hashes
caution
The recommended method for migrating users to SuperTokens is by importing users with their password hashes. You should only use the following method if you do not have access to your user's password hashes and still have access to your previous identity provider.
SuperTokens also supports the "just in time" user migration strategy for when password hashes cannot be exported from your legacy provider.
We will need to make the following customizations to SuperTokens authentication flows to support this strategy:
- Step 1) Prevent signups from users who exist in the external provider.
- To prevent duplicate accounts from being created, we block signups from users who have existing accounts with the external provider.
- Step 2) Create a SuperTokens account for users trying to sign in if they have an account with the external provider.
- We will modify the signin flow to check if the user signing in has an existing account with the external provider and not with SuperTokens. If their input credentials are valid, we create a SuperTokens user and import their user data.
- Step 3) Create a SuperTokens account for users who have an account with the external provider but have forgotten their password.
- Some users who have an account with the external provider and not with SuperTokens may have forgotten their passwords and will trigger the password reset flow. Since SuperTokens requires an existing account to send the reset password email to, we will need to modify the password reset flow to check that if the user needs to be migrated. If they do, we create a SuperTokens account with a temporary password, import their user data and continue the password reset flow.
- To ensure that users can only signin once they succesfully reset their passwords we add the
isUsingTemporaryPassword
flag to the account's metadata. We will also modify the signin flow to block signins from accounts with this metadata.
- Step 4) Remove the
isUsingTemporaryPassword
flag on successful password reset- Once the password has been successfully reset we check if the user has the
isUsingTemporaryPassword
flag set in the metadata. If they do we will clear the flag from the user's metadata.
- Once the password has been successfully reset we check if the user has the
- Step 5) Update the login flow to account for the
isUsingTemporaryPassword
flag- We also update the login flow to prevent signins from accounts who have the
isUsingTemporaryPassword
flag and if their input password does not match the one in the legacy auth provider. This is done so that users who started the password reset flow are forced to finish it.
- We also update the login flow to prevent signins from accounts who have the
#
Step 1) Prevent signups from users who exist in the external providerTo implement this change we will override the api that handles email-password login when initializing the recipe on the backend.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import EmailPassword from "supertokens-node/recipe/emailpassword"
EmailPassword.init({
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
signUpPOST: async function (input) {
let email = input.formFields.find((field) => field.id === "email")!.value as string;
// Check if the user signing in exists in the external provider
if (await doesUserExistInExternalProvider(email)) {
// Return status "EMAIL_ALREADY_EXISTS_ERROR" since the user already exists in the external provider
return {
status: "EMAIL_ALREADY_EXISTS_ERROR"
}
}
return originalImplementation.signUpPOST!(input);
},
}
},
}
})
async function doesUserExistInExternalProvider(email: string): Promise<boolean> {
// TODO: check if user with the input email exists in the external provider
return false;
}
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.recipe.emailpassword.interfaces import (
APIInterface,
APIOptions,
EmailAlreadyExistsError,
)
from supertokens_python.recipe.emailpassword.types import FormField
from typing import Dict, Any, Union, List
from supertokens_python.recipe.session.interfaces import SessionContainer
def override_email_password_apis(original_implementation: APIInterface):
original_sign_up = original_implementation.sign_up_post
async def sign_up(
form_fields: List[FormField],
tenant_id: str,
session: Union[SessionContainer, None],
should_try_linking_with_session_user: Union[bool, None],
api_options: APIOptions,
user_context: Dict[str, Any],
):
email = ""
for field in form_fields:
if field.id == "email":
email = field.value
# check if the user signing in exists in the external provider
if await does_user_exist_in_external_provider(email):
# Return SignUpEmailAlreadyExistsError since the user exists in the external provider
return EmailAlreadyExistsError()
return await original_sign_up(
form_fields,
tenant_id,
session,
should_try_linking_with_session_user,
api_options,
user_context,
)
original_implementation.sign_up_post = sign_up
return original_implementation
async def does_user_exist_in_external_provider(email: str):
# TODO: Check if a user with the input email exists in the external provider
return False
init(
app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
framework="...",
recipe_list=[
emailpassword.init(
override=emailpassword.InputOverrideConfig(
apis=override_email_password_apis,
)
)
],
)
import (
"errors"
"github.com/supertokens/supertokens-golang/recipe/emailpassword"
"github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
emailpassword.Init(&epmodels.TypeInput{
Override: &epmodels.OverrideStruct{
APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface {
// Copy the original implementation of the signUp function
originalSignUp := *originalImplementation.SignUpPOST
// Override the signUp function
(*originalImplementation.SignUpPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignUpPOSTResponse, error) {
email := ""
for _, formField := range formFields {
if formField.ID == "email" {
valueAsString, asStrOk := formField.Value.(string)
if !asStrOk {
return epmodels.SignUpPOSTResponse{}, errors.New("Should never come here as we check the type during validation")
}
email = valueAsString
}
}
// Check if the user signing in exists in the external provider
if doesUserExistInExternalProvider(email) {
// Return status "EMAIL_ALREADY_EXISTS_ERROR" since the user already exists in the external provider
return epmodels.SignUpPOSTResponse{
EmailAlreadyExistsError: &struct{}{},
}, nil
}
return originalSignUp(formFields, tenantId, options, userContext)
}
return originalImplementation
},
},
}),
},
})
}
func doesUserExistInExternalProvider(email string) bool {
// TODO: Check if user with the input email exists in the external provider
return false
}
We modify the signUpPOST
API to first check if the user signing up has an account with the external provider. If they do we return a EMAIL_ALREADY_EXISTS_ERROR
#
Step 2) Create a SuperTokens account for users trying to sign in if they have an account with the external providerTo implement this flow we will override the API that handles email-password login when initializing the recipe on the backend.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import SuperTokens from "supertokens-node"
import EmailPassword from "supertokens-node/recipe/emailpassword"
import EmailVerification from "supertokens-node/recipe/emailverification"
import { RecipeUserId } from "supertokens-node";
EmailPassword.init({
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
signInPOST: async function (input) {
// Check if an email-password user with the input email exists in SuperTokens
let email = input.formFields.find((field) => field.id === "email")!.value as string;
let password = input.formFields.find((field) => field.id === "password")!.value as string;
let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, {
email: email
}, undefined, input.userContext);
let emailPasswordUser = supertokensUsersWithSameEmail.find(u => {
return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined;
})
if (emailPasswordUser === undefined) {
// EmailPassword user with the input email does not exist in SuperTokens
// Check if the input credentials are valid in the external provider
let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password)
if (legacyUserInfo === undefined) {
// credentials are incorrect
return {
status: "WRONG_CREDENTIALS_ERROR"
}
}
// Call the signup function to create a new SuperTokens user.
let signUpResponse = await EmailPassword.signUp(input.tenantId, email, password, undefined, input.userContext);
if (signUpResponse.status !== "OK") {
throw new Error("Should never come here")
}
// Map the external provider's userId to the SuperTokens userId
await SuperTokens.createUserIdMapping({ superTokensUserId: signUpResponse.user.id, externalUserId: legacyUserInfo.user_id })
// Set the userId in the response to use the provider's userId
signUpResponse.user.id = legacyUserInfo.user_id
signUpResponse.user.loginMethods[0].recipeUserId = new RecipeUserId(legacyUserInfo.user_id);
signUpResponse.recipeUserId = new RecipeUserId(legacyUserInfo.user_id);
// We will also need to set the email verification status of the user
if (legacyUserInfo.isEmailVerified) {
// Generate an email verification token for the user
let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.recipeUserId, email, input.userContext);
if (generateEmailVerificationTokenResponse.status === "OK") {
// Verify the user's email
await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext);
}
}
}
return originalImplementation.signInPOST!(input)
},
}
},
}
})
async function validateAndGetUserInfoFromExternalProvider(email: string, password: string): Promise<{
user_id: string,
isEmailVerified: boolean
} | undefined> {
// TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info.
return undefined
}
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.asyncio import create_user_id_mapping
from supertokens_python.recipe.emailverification.asyncio import (
create_email_verification_token,
verify_email_using_token,
)
from supertokens_python.recipe.emailpassword.asyncio import sign_up
from supertokens_python.recipe.emailverification.interfaces import (
CreateEmailVerificationTokenOkResult,
)
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.recipe.emailpassword.interfaces import (
APIInterface,
APIOptions,
SignUpOkResult,
WrongCredentialsError,
)
from supertokens_python.recipe.emailpassword.types import FormField
from typing import Dict, Any, Union, List
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.asyncio import list_users_by_account_info
from supertokens_python.types import AccountInfo, RecipeUserId
def override_emailpassword_apis(original_implementation: APIInterface):
original_emailpassword_sign_in = original_implementation.sign_in_post
async def sign_in(
form_fields: List[FormField],
tenant_id: str,
session: Union[SessionContainer, None],
should_try_linking_with_session_user: Union[bool, None],
api_options: APIOptions,
user_context: Dict[str, Any],
):
email = ""
password = ""
for field in form_fields:
if field.id == "email":
email = field.value
if field.id == "password":
password = field.value
# Check if an email-password user with the input email exists in SuperTokens
supertokens_user_with_same_email = await list_users_by_account_info(
tenant_id, AccountInfo(email=email), False, user_context
)
emailpassword_user = next(
(
user
for user in supertokens_user_with_same_email
if any(
lm.recipe_id == "emailpassword" and lm.has_same_email_as(email)
for lm in user.login_methods
)
),
None,
)
if emailpassword_user is None:
# EmailPassword user with the input email does not exist in SuperTokens
# Check if the input credentials valid in the external provider
legacy_user_info = await validate_and_get_user_info_from_external_provider(
email, password
)
if legacy_user_info is None:
# Credentials are incorrect
return WrongCredentialsError()
# Call the sign_up function to create a new SuperTokens user.
response = await sign_up(tenant_id, email, password, None, user_context)
if not isinstance(response, SignUpOkResult):
raise Exception("Should never come here")
# Map the external provider's userId to the SuperTokens userId
await create_user_id_mapping(response.user.id, legacy_user_info.user_id)
# Set the userId in the response to use the provider's userId
response.user.id = legacy_user_info.user_id
response.user.login_methods[0].recipe_user_id = RecipeUserId(
legacy_user_info.user_id
)
response.recipe_user_id = RecipeUserId(legacy_user_info.user_id)
# We will also need to set the email verification status of the user
if legacy_user_info.isEmailVerified:
# Generate an email verification token for the user
generate_email_verification_response = (
await create_email_verification_token(
tenant_id,
response.recipe_user_id,
email,
user_context,
)
)
if isinstance(
generate_email_verification_response,
CreateEmailVerificationTokenOkResult,
):
await verify_email_using_token(
tenant_id,
generate_email_verification_response.token,
True,
user_context,
)
return await original_emailpassword_sign_in(
form_fields,
tenant_id,
session,
should_try_linking_with_session_user,
api_options,
user_context,
)
original_implementation.sign_in_post = sign_in
return original_implementation
class ExternalUserInfo:
def __init__(self, user_id: str, isEmailVerified: bool):
self.user_id: str = user_id
self.isEmailVerified: bool = isEmailVerified
async def validate_and_get_user_info_from_external_provider(
email: str, password: str
) -> Union[None, ExternalUserInfo]:
return None
init(
app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
framework="...",
recipe_list=[
emailpassword.init(
override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis)
)
],
)
import (
"errors"
"github.com/supertokens/supertokens-golang/recipe/emailpassword"
"github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels"
"github.com/supertokens/supertokens-golang/recipe/emailverification"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
emailpassword.Init(&epmodels.TypeInput{
Override: &epmodels.OverrideStruct{
APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface {
// Copy the original implementation of the signIn function
originalSignIn := *originalImplementation.SignInPOST
// Override the function
(*originalImplementation.SignInPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignInPOSTResponse, error) {
email := ""
password := ""
for _, formField := range formFields {
if formField.ID == "email" || formField.ID == "password" {
valueAsString, asStrOk := formField.Value.(string)
if !asStrOk {
return epmodels.SignInPOSTResponse{}, errors.New("Should never come here as we check the type during validation")
}
if formField.ID == "email" {
email = valueAsString
} else {
password = valueAsString
}
}
}
// Check if an email-password user with the input email exists in SuperTokens
emailPasswordUser, err := emailpassword.GetUserByEmail(tenantId, email)
if err != nil {
return epmodels.SignInPOSTResponse{}, err
}
if emailPasswordUser == nil {
// EmailPassword user with the input email does not exist in SuperTokens
// Check if the input credentials are valid in the external provider
legacyUserInfo := validateAndGetUserInfoFromExternalProvider(email, password)
if legacyUserInfo == nil {
return epmodels.SignInPOSTResponse{
WrongCredentialsError: &struct{}{},
}, nil
}
// Call the email-password signup function to create a new SuperTokens user.
response, err := emailpassword.SignUp(tenantId, email, password)
if err != nil {
return epmodels.SignInPOSTResponse{}, err
}
if response.OK == nil {
return epmodels.SignInPOSTResponse{}, errors.New("Should never come here")
}
// Map the external provider's userId to the SuperTokens userId
_, err = supertokens.CreateUserIdMapping(response.OK.User.ID, legacyUserInfo.userId, nil, nil)
if err != nil {
return epmodels.SignInPOSTResponse{}, err
}
// Set the userId in the response to use the provider's userId
response.OK.User.ID = legacyUserInfo.userId
// We will also need to set the email verification status of the user
if legacyUserInfo.isEmailVerified {
// Generate an email verification token for the user
generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, response.OK.User.ID, &email)
if err != nil {
return epmodels.SignInPOSTResponse{}, err
}
if generateEmailVerificationTokenResponse.OK != nil {
// Verify the user's email
emailverification.VerifyEmailUsingToken(tenantId, generateEmailVerificationTokenResponse.OK.Token)
}
}
}
return originalSignIn(formFields, tenantId, options, userContext)
}
return originalImplementation
},
},
}),
},
})
}
type ExternalUserInfo struct {
userId string
isEmailVerified bool
}
func validateAndGetUserInfoFromExternalProvider(email string, password string) *ExternalUserInfo {
// TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info.
return nil
}
The code above overrides the signInPOST
API with the following changes to achieve "just in time" migration:
- The first step is to determine if the user signing in needs to be migrated or not. We do this by checking if a user with the input email exists in the external auth provider and SuperTokens. If the user exists in the external auth provider and does not exist SuperTokens, we can determine that this user needs to be migrated.
- The next step is to validate the input credentials against the external provider. If the credentials are invalid we will throw a
WRONG_CREDENTIALS_ERROR
. If the credentials are valid we can call the SuperTokens login function with the input credentials to create a new SuperTokens user. - We now map the external
userId
to the SuperTokensuserId
. This will allow SuperTokens functions to refrence the user with the externaluserId
. - Finally, depending on the email verification status of the user in the external provider we will also verify the user's email in SuperTokens.
#
Step 3) Create a SuperTokens account for users who have an account with the external provider but have forgotten their password.Some users who do not have an account with SuperTokens but have an existing account with the external provider may have forgotten their passwords and will initiate a password reset. Since password resets require an existing SuperTokens account to send the password reset email to, the password reset flow will need to be modified to create a SuperTokens account if the user exists in the external provider.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import SuperTokens from "supertokens-node"
import EmailPassword from "supertokens-node/recipe/emailpassword"
import EmailVerification from "supertokens-node/recipe/emailverification"
import UserMetadata from "supertokens-node/recipe/usermetadata"
import { RecipeUserId } from "supertokens-node";
EmailPassword.init({
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
// Add overrides from the previous step
generatePasswordResetTokenPOST: async (input) => {
// Retrieve the email from the input
let email = input.formFields.find(i => i.id === "email")!.value as string;
// check if user exists in SuperTokens
let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, {
email
}, undefined, input.userContext);
let emailPasswordUser = supertokensUsersWithSameEmail.find(u => {
return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined;
})
if (emailPasswordUser === undefined) {
// User does not exist in SuperTokens
// Check if the user exists in the legacy provider and retrieve their data
let legacyUserData = await retrieveUserDataFromExternalProvider(email);
if (legacyUserData) {
// create a SuperTokens account for the user with a temporary password
let tempPassword = await generatePassword();
let signupResponse = await EmailPassword.signUp(input.tenantId, email, tempPassword, undefined, input.userContext);
if (signupResponse.status === "OK") {
// If user is succesfully created we will map the legacy id to their SuperTokens Id.
await SuperTokens.createUserIdMapping({
superTokensUserId: signupResponse.user.id,
externalUserId: legacyUserData.user_id
})
signupResponse.user.id = legacyUserData.user_id
signupResponse.user.loginMethods[0].recipeUserId = new RecipeUserId(legacyUserData.user_id);
signupResponse.recipeUserId = new RecipeUserId(legacyUserData.user_id);
// We will also need to set the email verification status of the user
if (legacyUserData.isEmailVerified) {
// Generate an email verification token for the user
let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signupResponse.recipeUserId, email, input.userContext);
if (generateEmailVerificationTokenResponse.status === "OK") {
// Verify the user's email
await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext);
}
}
// We also need to identify that the user is using a temporary password. We do through the userMetadata recipe
UserMetadata.updateUserMetadata(legacyUserData.user_id, { isUsingTemporaryPassword: true })
} else {
throw new Error("Should never come here")
}
}
}
return await originalImplementation.generatePasswordResetTokenPOST!(input);
},
}
}
}
})
async function generatePassword(): Promise<string> {
// TODO: generate a random password
return ""
}
async function retrieveUserDataFromExternalProvider(email: string): Promise<{
user_id: string,
isEmailVerified: boolean
} | undefined> {
// TODO: retrieve user data if a user with the input email exists in the external provider.
return undefined;
}
from supertokens_python import init, InputAppInfo
from supertokens_python.asyncio import create_user_id_mapping
from supertokens_python.recipe import emailpassword
from supertokens_python.asyncio import list_users_by_account_info
from supertokens_python.types import AccountInfo, RecipeUserId
from supertokens_python.recipe.emailverification.asyncio import (
create_email_verification_token,
verify_email_using_token,
)
from supertokens_python.recipe.emailpassword.types import FormField
from supertokens_python.recipe.emailpassword.asyncio import sign_up
from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata
from supertokens_python.recipe.emailpassword.interfaces import (
APIInterface,
APIOptions,
SignUpOkResult,
)
from supertokens_python.recipe.emailverification.interfaces import (
CreateEmailVerificationTokenOkResult,
)
from typing import Union, Dict, Any, List
def override_emailpassword_apis(original_implementation: APIInterface):
original_generate_password_reset_token_post = (
original_implementation.generate_password_reset_token_post
)
async def generate_password_reset_token_post(
form_fields: List[FormField],
tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
):
# retrieve the email from the form fields
email = None
for field in form_fields:
if field.id is "email":
email = field.value
if email is None:
raise Exception("Should never come here")
# Check if an email-password user with the input email exists in SuperTokens
supertokens_user_with_same_email = await list_users_by_account_info(
tenant_id, AccountInfo(email=email), False, user_context
)
emailpassword_user = next(
(
user
for user in supertokens_user_with_same_email
if any(
lm.recipe_id == "emailpassword" and lm.has_same_email_as(email)
for lm in user.login_methods
)
),
None,
)
if emailpassword_user is None:
# EmailPassword user with the input email does not exist in SuperTokens
# Check if the user exists in the legacy provider and retrieve their data
legacy_user_data = await retrieve_user_data_from_external_provider(email)
if legacy_user_data is not None:
# Create a SuperTokens account for the user with a temporary password
tempPassword = await generate_password()
response = await sign_up(
tenant_id, email, tempPassword, None, user_context
)
if not isinstance(response, SignUpOkResult):
raise Exception("Should never come here")
# Map the SuperTokens userId to the legacy userId
await create_user_id_mapping(response.user.id, legacy_user_data.user_id)
response.user.id = legacy_user_data.user_id
response.user.login_methods[0].recipe_user_id = RecipeUserId(
legacy_user_data.user_id
)
response.recipe_user_id = RecipeUserId(legacy_user_data.user_id)
# We will also need to set the email verification status
if legacy_user_data.isEmailVerified:
# Generate an email verification token for the user
generate_email_verification_token_response = (
await create_email_verification_token(
tenant_id, response.recipe_user_id, email, user_context
)
)
if isinstance(
generate_email_verification_token_response,
CreateEmailVerificationTokenOkResult,
):
# Verify the user's email
await verify_email_using_token(
tenant_id,
generate_email_verification_token_response.token,
True,
user_context,
)
# We also need to identify that the user is using a temporary password. We do through the userMetadata recipe
await update_user_metadata(
legacy_user_data.user_id, {"isUsingTemporaryPassword": True}
)
return await original_generate_password_reset_token_post(
form_fields, tenant_id, api_options, user_context
)
original_implementation.generate_password_reset_token_post = (
generate_password_reset_token_post
)
return original_implementation
class ExternalUserInfo:
def __init__(self, user_id: str, isEmailVerified: bool):
self.user_id: str = user_id
self.isEmailVerified: bool = isEmailVerified
async def retrieve_user_data_from_external_provider(
email: str,
) -> Union[None, ExternalUserInfo]:
# TODO: Retrieve user data if a user with the input email exists in the external provider.
return None
async def generate_password():
# TODO: generate a random password
return ""
init(
app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
framework="...",
recipe_list=[
emailpassword.init(
override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis)
)
],
)
import (
"errors"
"github.com/supertokens/supertokens-golang/recipe/emailpassword"
"github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels"
"github.com/supertokens/supertokens-golang/recipe/emailverification"
"github.com/supertokens/supertokens-golang/recipe/usermetadata"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
emailpassword.Init(&epmodels.TypeInput{
Override: &epmodels.OverrideStruct{
APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface {
// Copy the original implementation of the function
originalGeneratePasswordResetTokenPOST := *originalImplementation.GeneratePasswordResetTokenPOST
// Override the API
(*originalImplementation.GeneratePasswordResetTokenPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.GeneratePasswordResetTokenPOSTResponse, error) {
// Retrieve email from the form fields
var email *string = nil
for _, field := range formFields {
if field.ID == "email" {
valueAsString, asStrOk := field.Value.(string)
if !asStrOk {
return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here as we check the type during validation")
}
email = &valueAsString
}
}
if email == nil {
return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here")
}
// Check if an email-password user with the input email exists in SuperTokens
emailPasswordUser, err := emailpassword.GetUserByEmail(tenantId, *email)
if err != nil {
return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err
}
if emailPasswordUser == nil {
// User does not exist in SuperTokens
// Check if the user exists in the legacy provider and retrieve their data
legacyUserInfo := retrieveUserDataFromExternalProvider(*email)
if legacyUserInfo != nil {
// Create a SuperTokens account for the user with a temporary password
tempPassword := generatePassword()
response, err := emailpassword.SignUp(tenantId, *email, tempPassword)
if err != nil {
return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err
}
if response.OK == nil {
return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("should never come here")
}
// Map the external provider's userId to the SuperTokens userId
_, err = supertokens.CreateUserIdMapping(response.OK.User.ID, legacyUserInfo.userId, nil, nil)
if err != nil {
return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err
}
// Set the userId in the response to use the provider's userId
response.OK.User.ID = legacyUserInfo.userId
// We will also need to set the email verification status of the user
if legacyUserInfo.isEmailVerified {
generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, response.OK.User.ID, &response.OK.User.Email)
if err != nil {
return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err
}
if generateEmailVerificationTokenResponse.OK != nil {
// Verify the user's email
_, err := emailverification.VerifyEmailUsingToken(tenantId, generateEmailVerificationTokenResponse.OK.Token)
if err != nil {
return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err
}
}
}
// We also need to identify that the user is using a temporary password. We do through the UserMetadata recipe
_, err = usermetadata.UpdateUserMetadata(legacyUserInfo.userId, map[string]interface{}{
"isUsingTemporaryPassword": true,
})
if err != nil {
return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err
}
}
}
return originalGeneratePasswordResetTokenPOST(formFields, tenantId, options, userContext)
}
return originalImplementation
},
},
}),
usermetadata.Init(nil),
},
})
}
type ExternalUserInfo struct {
userId string
isEmailVerified bool
}
func retrieveUserDataFromExternalProvider(email string) *ExternalUserInfo {
// TODO: Retrieve user info from external provider if account with input email exists.
return nil
}
func generatePassword() string {
// TODO: generate a random password
return ""
}
The code above overrides the generatePasswordResetTokenPOST
API. This is the first step in the password reset flow and is responsible for generating the password reset token to be sent with the reset password email.
- Similar to the previous step, we need to determine whether to migrate the user or not.
- The next step is to create a SuperTokens account with a temporary password, the password can be a random string since it will be reset by the user when they complete the reset password flow.
- We now map the external
userId
(the userId from the external provider) to the SuperTokensuserId
. This will allow SuperTokens functions to refrence the user with the externaluserId
. - Depending on the email verification status of the user in the external provider we will also verify the user's email in SuperTokens.
- We assign the
isUsingTemporaryPassword
flag to user's metadata since the account was generated with a temporary password. This is done to prevent signins until the password is successfully reset.
isUsingTemporaryPassword
flag on successful password reset#
Step 4) Remove the If the password reset flow is successfully completed we will need to check if the user has isUsingTemporaryPassword
set in their metadata and remove it if it exists.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import EmailPassword from "supertokens-node/recipe/emailpassword"
import UserMetadata from "supertokens-node/recipe/usermetadata"
EmailPassword.init({
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
// TODO: implementation details in previous step
passwordResetPOST: async function (input) {
let response = await originalImplementation.passwordResetPOST!(input);
if (response.status === "OK") {
let usermetadata = await UserMetadata.getUserMetadata(response.user.id, input.userContext)
if (usermetadata.status === "OK" && usermetadata.metadata.isUsingTemporaryPassword) {
// Since the password reset we can remove the isUsingTemporaryPassword flag
await UserMetadata.updateUserMetadata(response.user.id, { isUsingTemporaryPassword: null })
}
}
return response
}
}
}
}
})
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.recipe.emailpassword.types import FormField
from supertokens_python.recipe.usermetadata.asyncio import (
get_user_metadata,
update_user_metadata,
)
from supertokens_python.recipe.emailpassword.interfaces import (
APIInterface,
APIOptions,
PasswordResetPostOkResult,
)
from typing import Dict, Any, List
def override_emailpassword_apis(original_implementation: APIInterface):
original_password_reset_post = original_implementation.password_reset_post
async def password_reset_post(
form_fields: List[FormField],
token: str,
tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
):
response = await original_password_reset_post(
form_fields, token, tenant_id, api_options, user_context
)
if (
isinstance(response, PasswordResetPostOkResult)
):
# Check that the user has the isUsingTemporaryPassword flag set in their metadata
metadata_result = await get_user_metadata(response.user.id, user_context)
if (
"isUsingTemporaryPassword" in metadata_result.metadata
and metadata_result.metadata["isUsingTemporaryPassword"] is True
):
# Since the password has been successfully reset, we can remove the isUsingTemporaryPassword flag
await update_user_metadata(
response.user.id, {"isUsingTemporaryPassword": None}
)
return response
original_implementation.password_reset_post = password_reset_post
return original_implementation
init(
app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
framework="...",
recipe_list=[
emailpassword.init(
override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis)
)
],
)
import (
"github.com/supertokens/supertokens-golang/recipe/emailpassword"
"github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels"
"github.com/supertokens/supertokens-golang/recipe/usermetadata"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
emailpassword.Init(&epmodels.TypeInput{
Override: &epmodels.OverrideStruct{
APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface {
// Copy the original implementation of the passwordReset
originalPasswordResetPOST := *originalImplementation.PasswordResetPOST
// Then we override the API
(*originalImplementation.PasswordResetPOST) = func(formFields []epmodels.TypeFormField, token, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.ResetPasswordPOSTResponse, error) {
response, err := originalPasswordResetPOST(formFields, token, tenantId, options, userContext)
if err != nil {
return epmodels.ResetPasswordPOSTResponse{}, err
}
if response.OK != nil {
metadata, err := usermetadata.GetUserMetadata(*response.OK.UserId)
if err != nil {
return epmodels.ResetPasswordPOSTResponse{}, err
}
isUsingTemporaryPassword, ok := metadata["isUsingTemporaryPassword"]
if ok && isUsingTemporaryPassword.(bool) {
// Since the password is reset we can remove the isUsingTemporaryPassword flag
_, err = usermetadata.UpdateUserMetadata(*response.OK.UserId, map[string]interface{}{
"isUsingTemporaryPassword": nil,
})
if err != nil {
return epmodels.ResetPasswordPOSTResponse{}, err
}
}
}
return response, nil
}
return originalImplementation
},
},
}),
usermetadata.Init(nil),
},
})
}
The code above overrides the passwordResetPOST
API and is a continuation of the password reset flow:
- On a successful password reset we check if the user has the
isUsingTemporaryPassword
flag set in their metadata and remove it If it exists.
isUsingTemporaryPassword
flag#
Step 5) Update the login flow to account for the Apart from the changes we made in Step 1, we also need to account for users who have a initiated password reset but have not completed the flow. There are two cases we need to handle:
- Prevent signin from accounts that have temporary passwords.
- If, for any reason, the user tries to sign into their account with the temporary password, then the login method should be blocked.
- If a user initiates a password reset but remembers their password, they should be able to sign in.
- In this case the user should be able to login and the database should be updated to reflect the new password.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import SuperTokens from "supertokens-node";
import EmailPassword from "supertokens-node/recipe/emailpassword"
import EmailVerification from "supertokens-node/recipe/emailverification";
import UserMetadata from "supertokens-node/recipe/usermetadata";
import { RecipeUserId } from "supertokens-node";
EmailPassword.init({
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
signInPOST: async function (input) {
// Check if an email-password user with the input email exists in SuperTokens
let email = input.formFields.find((field) => field.id === "email")!.value as string;
let password = input.formFields.find((field) => field.id === "password")!.value as string;
let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, {
email: email
}, undefined, input.userContext);
let emailPasswordUser = supertokensUsersWithSameEmail.find(u => {
return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined;
})
if (emailPasswordUser === undefined) {
// EmailPassword user with the input email does not exist in SuperTokens
// Check if the input credentials are valid in the external provider
let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password)
if (legacyUserInfo === undefined) {
// credentials are incorrect
return {
status: "WRONG_CREDENTIALS_ERROR"
}
}
// Call the signup function to create a new SuperTokens user.
let signUpResponse = await EmailPassword.signUp(input.tenantId, email, password, undefined, input.userContext);
if (signUpResponse.status !== "OK") {
throw new Error("Should never come here")
}
// Map the external provider's userId to the SuperTokens userId
await SuperTokens.createUserIdMapping({ superTokensUserId: signUpResponse.user.id, externalUserId: legacyUserInfo.user_id })
// Set the userId in the response to use the provider's userId
signUpResponse.user.id = legacyUserInfo.user_id
signUpResponse.user.loginMethods[0].recipeUserId = new RecipeUserId(legacyUserInfo.user_id);
// We will also need to set the email verification status of the user
if (legacyUserInfo.isEmailVerified) {
// Generate an email verification token for the user
let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.recipeUserId, email, input.userContext);
if (generateEmailVerificationTokenResponse.status === "OK") {
// Verify the user's email
await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext);
}
}
emailPasswordUser = signUpResponse.user;
}
// Check if the user signing in has a temporary password
let userMetadata = await UserMetadata.getUserMetadata(emailPasswordUser.id, input.userContext)
if (userMetadata.status === "OK" && userMetadata.metadata.isUsingTemporaryPassword) {
// Check if the input credentials are valid in the external provider
let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password);
if (legacyUserInfo) {
let loginMethod = emailPasswordUser.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(email));
// Update the user's password with the correct password
EmailPassword.updateEmailOrPassword({
recipeUserId: loginMethod!.recipeUserId,
password: password,
applyPasswordPolicy: false
})
// Update the user's metadata to remove the isUsingTemporaryPassword flag
UserMetadata.updateUserMetadata(emailPasswordUser.id, { isUsingTemporaryPassword: null })
} else {
return {
status: "WRONG_CREDENTIALS_ERROR"
}
}
}
return originalImplementation.signInPOST!(input)
},
}
},
}
})
async function validateAndGetUserInfoFromExternalProvider(email: string, password: string): Promise<{
user_id: string,
isEmailVerified: boolean
} | undefined> {
// TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info.
return undefined
}
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.asyncio import create_user_id_mapping
from supertokens_python.recipe.emailverification.asyncio import (
create_email_verification_token,
verify_email_using_token,
)
from supertokens_python.recipe.emailpassword.asyncio import (
sign_up,
update_email_or_password,
)
from supertokens_python.recipe.emailverification.interfaces import (
CreateEmailVerificationTokenOkResult,
)
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.recipe.emailpassword.interfaces import (
APIInterface,
APIOptions,
SignUpOkResult,
WrongCredentialsError,
)
from supertokens_python.recipe.emailpassword.types import FormField
from typing import Dict, Any, Union, List
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.asyncio import list_users_by_account_info
from supertokens_python.types import AccountInfo, RecipeUserId
from supertokens_python.recipe.usermetadata.asyncio import (
get_user_metadata,
update_user_metadata,
)
def override_emailpassword_apis(original_implementation: APIInterface):
original_emailpassword_sign_in = original_implementation.sign_in_post
async def sign_in(
form_fields: List[FormField],
tenant_id: str,
session: Union[SessionContainer, None],
should_try_linking_with_session_user: Union[bool, None],
api_options: APIOptions,
user_context: Dict[str, Any],
):
email = ""
password = ""
for field in form_fields:
if field.id == "email":
email = field.value
if field.id == "password":
password = field.value
# Check if an email-password user with the input email exists in SuperTokens
supertokens_user_with_same_email = await list_users_by_account_info(
tenant_id, AccountInfo(email=email), False, user_context
)
emailpassword_user = next(
(
user
for user in supertokens_user_with_same_email
if any(
lm.recipe_id == "emailpassword" and lm.has_same_email_as(email)
for lm in user.login_methods
)
),
None,
)
if emailpassword_user is None:
# EmailPassword user with the input email does not exist in SuperTokens
# Check if the input credentials valid in the external provider
legacy_user_info = await validate_and_get_user_info_from_external_provider(
email, password
)
if legacy_user_info is None:
# Credentials are incorrect
return WrongCredentialsError()
# Call the sign_up function to create a new SuperTokens user.
response = await sign_up(tenant_id, email, password, None, user_context)
if not isinstance(response, SignUpOkResult):
raise Exception("Should never come here")
# Map the external provider's userId to the SuperTokens userId
await create_user_id_mapping(response.user.id, legacy_user_info.user_id)
# Set the userId in the response to use the provider's userId
response.user.id = legacy_user_info.user_id
response.user.login_methods[0].recipe_user_id = RecipeUserId(
legacy_user_info.user_id
)
response.recipe_user_id = RecipeUserId(legacy_user_info.user_id)
# We will also need to set the email verification status of the user
if legacy_user_info.isEmailVerified:
# Generate an email verification token for the user
generate_email_verification_response = (
await create_email_verification_token(
tenant_id,
response.recipe_user_id,
email,
user_context,
)
)
if isinstance(
generate_email_verification_response,
CreateEmailVerificationTokenOkResult,
):
await verify_email_using_token(
tenant_id,
generate_email_verification_response.token,
True,
user_context,
)
emailpassword_user = response.user
# Check if the user signing in has a temporary password
metadata_result = await get_user_metadata(emailpassword_user.id)
if (
"isUsingTemporaryPassword" in metadata_result.metadata
and metadata_result.metadata["isUsingTemporaryPassword"] is True
):
# Check if the input credentials are valid in the external provider
legacy_user_info = await validate_and_get_user_info_from_external_provider(
email, password
)
if legacy_user_info is not None:
# Find the emailpassword login method for the user
login_method = next(
(
lm
for lm in emailpassword_user.login_methods
if lm.recipe_id == "emailpassword" and lm.email == email
),
None,
)
assert login_method is not None
# Update the user's password with the correct password
await update_email_or_password(
login_method.recipe_user_id,
None,
password,
False,
tenant_id,
user_context,
)
# Update the user's metadata to remove the isUsingTemporaryPassword flag
await update_user_metadata(
emailpassword_user.id, {"isUsingTemporaryPassword": None}
)
else:
return WrongCredentialsError()
return await original_emailpassword_sign_in(
form_fields,
tenant_id,
session,
should_try_linking_with_session_user,
api_options,
user_context,
)
original_implementation.sign_in_post = sign_in
return original_implementation
class ExternalUserInfo:
def __init__(self, user_id: str, isEmailVerified: bool):
self.user_id: str = user_id
self.isEmailVerified: bool = isEmailVerified
async def validate_and_get_user_info_from_external_provider(
email: str, password: str
) -> Union[None, ExternalUserInfo]:
return None
init(
app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
framework="...",
recipe_list=[
emailpassword.init(
override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis)
)
],
)
import (
"errors"
"github.com/supertokens/supertokens-golang/recipe/emailpassword"
"github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels"
"github.com/supertokens/supertokens-golang/recipe/emailverification"
"github.com/supertokens/supertokens-golang/recipe/usermetadata"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
emailpassword.Init(&epmodels.TypeInput{
Override: &epmodels.OverrideStruct{
APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface {
// Copy the original implementation of the signIn function
originalSignIn := *originalImplementation.SignInPOST
// Override the function
(*originalImplementation.SignInPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignInPOSTResponse, error) {
email := ""
password := ""
for _, formField := range formFields {
if formField.ID == "email" || formField.ID == "password" {
valueAsString, asStrOk := formField.Value.(string)
if !asStrOk {
return epmodels.SignInPOSTResponse{}, errors.New("Should never come here as we check the type during validation")
}
if formField.ID == "email" {
email = valueAsString
} else {
password = valueAsString
}
}
}
// Check if an email-password user with the input email exists in SuperTokens
emailPasswordUser, err := emailpassword.GetUserByEmail(tenantId, email)
if err != nil {
return epmodels.SignInPOSTResponse{}, err
}
if emailPasswordUser == nil {
// EmailPassword user with the input email does not exist in SuperTokens
// Check if the input credentials are valid in the external provider
legacyUserInfo := validateAndGetUserInfoFromExternalProvider(email, password)
if legacyUserInfo == nil {
return epmodels.SignInPOSTResponse{
WrongCredentialsError: &struct{}{},
}, nil
}
// Call the email-password signup function to create a new SuperTokens user.
response, err := emailpassword.SignUp(tenantId, email, password)
if err != nil {
return epmodels.SignInPOSTResponse{}, err
}
if response.OK == nil {
return epmodels.SignInPOSTResponse{}, errors.New("Should never come here")
}
// Map the external provider's userId to the SuperTokens userId
_, err = supertokens.CreateUserIdMapping(response.OK.User.ID, legacyUserInfo.userId, nil, nil)
// Set the userId in the response to use the provider's userId
response.OK.User.ID = legacyUserInfo.userId
// We will also need to set the email verification status of the user
if legacyUserInfo.isEmailVerified {
// Generate an email verification token for the user
generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, response.OK.User.ID, &email)
if err != nil {
return epmodels.SignInPOSTResponse{}, err
}
if generateEmailVerificationTokenResponse.OK != nil {
// Verify the user's email
_, err = emailverification.VerifyEmailUsingToken(tenantId, generateEmailVerificationTokenResponse.OK.Token)
if err != nil {
return epmodels.SignInPOSTResponse{}, err
}
}
}
emailPasswordUser = &response.OK.User
}
// Check if the user signing in has a temporary password
metadata, err := usermetadata.GetUserMetadata(emailPasswordUser.ID)
if err != nil {
return epmodels.SignInPOSTResponse{}, err
}
isUsingTemporaryPassword, ok := metadata["isUsingTemporaryPassword"]
if ok && isUsingTemporaryPassword.(bool) {
// Check that the input credentials are valid in the external provider
legacyUserInfo := validateAndGetUserInfoFromExternalProvider(email, password)
if legacyUserInfo != nil {
// Replace the temporary password with the valid password
usePasswordPolicy := false
_, err = emailpassword.UpdateEmailOrPassword(emailPasswordUser.ID, nil, &password, &usePasswordPolicy, &tenantId)
if err != nil {
return epmodels.SignInPOSTResponse{}, err
}
// Update the user's metadata to remove the isUsingTemporaryPassword flag
_, err = usermetadata.UpdateUserMetadata(emailPasswordUser.ID, map[string]interface{}{
"isUsingTemporaryPassword": nil,
})
if err != nil {
return epmodels.SignInPOSTResponse{}, err
}
} else {
return epmodels.SignInPOSTResponse{
WrongCredentialsError: &struct{}{},
}, nil
}
}
return originalSignIn(formFields, tenantId, options, userContext)
}
return originalImplementation
},
},
}),
usermetadata.Init(nil),
},
})
}
type ExternalUserInfo struct {
userId string
isEmailVerified bool
}
func validateAndGetUserInfoFromExternalProvider(email string, password string) *ExternalUserInfo {
// TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info.
return nil
}
The code above adds the following changes to the signInPOST
API:
- Adds an additional check where if a user exists in SuperTokens, we check if they have the
isUsingTemporaryPassword
flag set in their metadata. - If the flag exists, we check if the input credentials are valid in the external provider. If they are we update the account with the new password and continue the login flow.
- If the input credentials are invalid in the external provider, we return a
WRONG_CREDENTIALS_ERROR
.
#
User migration edge cases that are addressedThis stratergy takes into account the following edge cases to ensure a smooth migration experience:
- Users who have not been migrated over to SuperTokens forgets their passwords and tries the password reset flow
- In this situation, the regular password reset flow will not work since password resets require an existing account.
- The changes proposed in Step 3 and Step 4 resolve the this edge case.
- User starts the password reset flow and attempts to sign in with a temporary password
- If, for any reason, the user tries to sign into their account with the temporary password, then the login method should be blocked.
- The changes proposed in Step 5 allows for this flow
- User starts the password reset flow but they remember their password and try to login with the valid password.
- In this scenario if the user starts the password reset flow, a new SuperTokens account with a temporary password is created. Instead of completing the password reset flow they remember their password and try to sign in. In this case the user should be able to successfully sign in and the account should be updated with the valid password.
- The changes proposed in Step 5 allows for this flow.
#
When can I stop using my legacy authentication provider?Your migration period could be decided by either of the following factors:
- A time window (2-3 months) within which "just-in-time" migration is active.
- A user migration threshold where, after a certain percentage of the userbase is migrated, the migration period ends.
After the migration period ends you have to make the following changes to stop automatic user migration:
- Remove all migration related override changes in your backend.
- Take the remaining users' emails and call the signup function with a secure randomized password.
- Email users encouraging them to go through the password reset flow.