diff --git a/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx b/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx index 7a71670f6..712d4ab85 100644 --- a/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx +++ b/v2/emailpassword/migration/account-creation/ep-migration-without-password-hash.mdx @@ -36,7 +36,7 @@ We will need to make the following customizations to SuperTokens authentication ## Step 1) Prevent signups from users who exist in the external provider -To implement this change we will override the function that handles email-password login when initializing the `ThirdPartyEmailPassword` recipe on the backend. +To implement this change we will override the api that handles email-password login when initializing the recipe on the backend. @@ -46,18 +46,19 @@ import EmailPassword from "supertokens-node/recipe/emailpassword" EmailPassword.init({ override: { - functions: (originalImplementation) => { + apis: (originalImplementation) => { return { ...originalImplementation, - signUp: async function(input) { + signUpPOST: async function (input) { + let email = input.formFields.find((field) => field.id === "email")!.value; // Check if the user signing in exists in the external provider - if(await doesUserExistInExternalProvider(input.email)){ + 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.signUp(input); + return originalImplementation.signUpPOST!(input); }, } }, @@ -77,22 +78,40 @@ async function doesUserExistInExternalProvider(email: string): Promise ```python from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword -from supertokens_python.recipe.emailpassword.interfaces import RecipeInterface, SignUpOkResult, SignUpEmailAlreadyExistsError -from typing import Dict, Any, Union +from supertokens_python.recipe.emailpassword.interfaces import ( + APIInterface, + SignUpPostOkResult, + SignUpPostEmailAlreadyExistsError, + APIOptions, +) +from supertokens_python.types import GeneralErrorResponse +from supertokens_python.recipe.emailpassword.types import FormField +from typing import Dict, Any, Union, List -def override_email_password_functions(original_implementation: RecipeInterface): - original_sign_up = original_implementation.sign_up +def override_email_password_apis(original_implementation: APIInterface): + original_sign_up = original_implementation.sign_up_post - async def sign_up(email: str, password: str, tenant_id: str, user_context: Dict[str, Any]) -> Union[SignUpOkResult, SignUpEmailAlreadyExistsError]: + async def sign_up( + form_fields: List[FormField], + tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], + ) -> Union[ + SignUpPostOkResult, SignUpPostEmailAlreadyExistsError, GeneralErrorResponse + ]: + 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 SignUpEmailAlreadyExistsError() + return SignUpPostEmailAlreadyExistsError() - return await original_sign_up(email, password, tenant_id, user_context) + return await original_sign_up(form_fields, tenant_id, api_options, user_context) - original_implementation.sign_up = sign_up + original_implementation.sign_up_post = sign_up return original_implementation @@ -102,16 +121,15 @@ async def does_user_exist_in_external_provider(email: str): init( - app_info=InputAppInfo( - api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework="...", # type: ignore recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( - functions=override_email_password_functions, + apis=override_email_password_apis, ) ) - ] + ], ) ``` @@ -131,21 +149,27 @@ func main() { RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ - Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { + APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // Copy the original implementation of the signUp function - originalSignUp := *originalImplementation.SignUp + originalSignUp := *originalImplementation.SignUpPOST // Override the signUp function - (*originalImplementation.SignUp) = func(email, password string, tenantId string, userContext supertokens.UserContext) (epmodels.SignUpResponse, error) { + (*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" { + email = formField.Value + } + } // 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.SignUpResponse{ + return epmodels.SignUpPOSTResponse{ EmailAlreadyExistsError: &struct{}{}, }, nil } - return originalSignUp(email, password, tenantId, userContext) + return originalSignUp(formFields, tenantId, options, userContext) } return originalImplementation }, @@ -165,11 +189,11 @@ func doesUserExistInExternalProvider(email string) bool { -We modify the `signUp` function 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` +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 provider -To implement this flow we will override the function that handles email-password login when initializing the `ThirdPartyEmailPassword` recipe on the backend. +To implement this flow we will override the API that handles email-password login when initializing the recipe on the backend. @@ -182,22 +206,24 @@ import { RecipeUserId } from "supertokens-node"; EmailPassword.init({ override: { - functions: (originalImplementation) => { + apis: (originalImplementation) => { return { ...originalImplementation, - signIn: async function (input) { + 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; + let password = input.formFields.find((field) => field.id === "password")!.value; let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { - email: input.email + email: email }, undefined, input.userContext); let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { - return u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "emailpassword") !== undefined; + 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(input.email, input.password) + let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password) if (legacyUserInfo === undefined) { // credentials are incorrect return { @@ -206,14 +232,14 @@ EmailPassword.init({ } // Call the signup function to create a new SuperTokens user. - let signUpResponse = await EmailPassword.signUp(input.tenantId, input.email, input.password, undefined, input.userContext); + 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); @@ -221,18 +247,16 @@ EmailPassword.init({ // 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, input.email, input.userContext); + 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 signUpResponse; } - return originalImplementation.signIn(input) + return originalImplementation.signInPOST!(input) }, } }, @@ -256,17 +280,48 @@ async function validateAndGetUserInfoFromExternalProvider(email: string, passwor 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.emailverification.asyncio import ( + create_email_verification_token, + verify_email_using_token, +) from supertokens_python.recipe.emailpassword.asyncio import get_user_by_email, sign_up -from supertokens_python.recipe.emailpassword.interfaces import RecipeInterface, SignInOkResult, SignInWrongCredentialsError, SignUpEmailAlreadyExistsError -from supertokens_python.recipe.emailverification.interfaces import CreateEmailVerificationTokenOkResult -from typing import Dict, Any, Union +from supertokens_python.recipe.emailpassword.interfaces import ( + SignUpEmailAlreadyExistsError, +) +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, + SignInPostOkResult, + SignInPostWrongCredentialsError, + APIOptions, +) +from supertokens_python.types import GeneralErrorResponse +from supertokens_python.recipe.emailpassword.types import FormField +from typing import Dict, Any, Union, List -def override_emailpassword_functions(original_implementation: RecipeInterface): - original_emailpassword_sign_in = original_implementation.sign_in +def override_emailpassword_apis(original_implementation: APIInterface): + original_emailpassword_sign_in = original_implementation.sign_in_post - async def sign_in(email: str, password: str, tenant_id: str, user_context: Dict[str, Any]) -> Union[SignInOkResult, SignInWrongCredentialsError]: + async def sign_in( + form_fields: List[FormField], + tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], + ) -> Union[ + SignInPostOkResult, SignInPostWrongCredentialsError, GeneralErrorResponse + ]: + 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 emailpassword_user = await get_user_by_email(tenant_id, email, user_context) @@ -274,10 +329,11 @@ def override_emailpassword_functions(original_implementation: RecipeInterface): # 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) + email, password + ) if legacy_user_info is None: # Credentials are incorrect - return SignInWrongCredentialsError() + return SignInPostWrongCredentialsError() # Call the sign_up function to create a new SuperTokens user. response = await sign_up(email, password, tenant_id, user_context) @@ -285,32 +341,38 @@ def override_emailpassword_functions(original_implementation: RecipeInterface): raise Exception("Should never come here") # Map the external provider's userId to the SuperTokens userId - await create_user_id_mapping(response.user.user_id, - legacy_user_info.user_id) + await create_user_id_mapping( + response.user.user_id, legacy_user_info.user_id + ) # Set the userId in the response to use the provider's userId response.user.user_id = 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.user.user_id, - email, - user_context, + generate_email_verification_response = ( + await create_email_verification_token( + tenant_id, + response.user.user_id, + email, + user_context, + ) ) - if isinstance(generate_email_verification_response, CreateEmailVerificationTokenOkResult): + if isinstance( + generate_email_verification_response, + CreateEmailVerificationTokenOkResult, + ): await verify_email_using_token( tenant_id, - generate_email_verification_response.token, + generate_email_verification_response.token, user_context, ) - return SignInOkResult(response.user) - - return await original_emailpassword_sign_in(email, password, tenant_id, user_context) + return await original_emailpassword_sign_in( + form_fields, tenant_id, api_options, user_context + ) - original_implementation.sign_in = sign_in + original_implementation.sign_in_post = sign_in return original_implementation @@ -320,21 +382,20 @@ class ExternalUserInfo: self.isEmailVerified: bool = isEmailVerified -async def validate_and_get_user_info_from_external_provider(email: str, password: str) -> Union[None, ExternalUserInfo]: +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='...', # type: ignore + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework="...", # type: ignore recipe_list=[ emailpassword.init( - override=emailpassword.InputOverrideConfig( - functions=override_emailpassword_functions - ) + override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ) - ] + ], ) ``` @@ -357,18 +418,28 @@ func main() { RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ - Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { + APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // Copy the original implementation of the signIn function - originalSignIn := *originalImplementation.SignIn + originalSignIn := *originalImplementation.SignInPOST // Override the function - (*originalImplementation.SignIn) = func(email, password, tenantId string, userContext supertokens.UserContext) (epmodels.SignInResponse, error) { + (*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" { + email = formField.Value + } + if formField.ID == "password" { + password = formField.Value + } + } // Check if an email-password user with the input email exists in SuperTokens emailPasswordUser, err := emailpassword.GetUserByEmail(tenantId, email) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } if emailPasswordUser == nil { @@ -377,7 +448,7 @@ func main() { legacyUserInfo := validateAndGetUserInfoFromExternalProvider(email, password) if legacyUserInfo == nil { - return epmodels.SignInResponse{ + return epmodels.SignInPOSTResponse{ WrongCredentialsError: &struct{}{}, }, nil } @@ -385,17 +456,17 @@ func main() { // Call the email-password signup function to create a new SuperTokens user. response, err := emailpassword.SignUp(tenantId, email, password) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } if response.OK == nil { - return epmodels.SignInResponse{}, errors.New("Should never come here") + 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.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } // Set the userId in the response to use the provider's userId response.OK.User.ID = legacyUserInfo.userId @@ -405,7 +476,7 @@ func main() { // Generate an email verification token for the user generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, response.OK.User.ID, &email) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } if generateEmailVerificationTokenResponse.OK != nil { @@ -413,14 +484,9 @@ func main() { emailverification.VerifyEmailUsingToken(tenantId, generateEmailVerificationTokenResponse.OK.Token) } } - - return epmodels.SignInResponse{ - OK: &struct{ User epmodels.User }{response.OK.User}, - }, nil - } - return originalSignIn(email, password, tenantId, userContext) + return originalSignIn(formFields, tenantId, options, userContext) } return originalImplementation @@ -446,7 +512,7 @@ func validateAndGetUserInfoFromExternalProvider(email string, password string) * -The code above overrides the `signIn` function with the following changes to achieve "**just in time**" migration: +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`. @@ -471,15 +537,10 @@ import UserMetadata from "supertokens-node/recipe/usermetadata" EmailPassword.init({ override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - // TODO: implementation details in previous step - } - }, 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; @@ -828,12 +889,6 @@ import UserMetadata from "supertokens-node/recipe/usermetadata" EmailPassword.init({ override: { - functions: (originalImplementaion) => { - return { - ...originalImplementaion - // TODO: implentation details in previous step - } - }, apis: (originalImplementation) => { return { ...originalImplementation, @@ -1015,22 +1070,24 @@ import { RecipeUserId } from "supertokens-node"; EmailPassword.init({ override: { - functions: (originalImplementation) => { + apis: (originalImplementation) => { return { ...originalImplementation, - signIn: async function (input) { + 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; + let password = input.formFields.find((field) => field.id === "password")!.value; let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { - email: input.email + email: email }, undefined, input.userContext); let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { - return u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "emailpassword") !== undefined; + 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(input.email, input.password) + let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password) if (legacyUserInfo === undefined) { // credentials are incorrect return { @@ -1039,7 +1096,7 @@ EmailPassword.init({ } // Call the signup function to create a new SuperTokens user. - let signUpResponse = await EmailPassword.signUp(input.tenantId, input.email, input.password, undefined, input.userContext); + let signUpResponse = await EmailPassword.signUp(input.tenantId, email, password, undefined, input.userContext); if (signUpResponse.status !== "OK") { throw new Error("Should never come here") } @@ -1053,47 +1110,46 @@ EmailPassword.init({ // 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, input.email, input.userContext); + 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 signUpResponse; } // highlight-start + emailPasswordUser = supertokensUsersWithSameEmail.find(u => { + return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined; + }) + if (emailPasswordUser === undefined) { + throw new Error("Should never come here") + } // 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(input.email, input.password); + let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password); if (legacyUserInfo) { - let loginMethod = emailPasswordUser.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(input.email)); + 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: input.password, + password: password, applyPasswordPolicy: false }) // Update the user's metadata to remove the isUsingTemporaryPassword flag UserMetadata.updateUserMetadata(emailPasswordUser.id, { isUsingTemporaryPassword: null }) - + } else { return { - status: "OK", - user: emailPasswordUser, - recipeUserId: loginMethod!.recipeUserId + status: "WRONG_CREDENTIALS_ERROR" } } - return { - status: "WRONG_CREDENTIALS_ERROR" - } } // highlight-end - return originalImplementation.signIn(input) + return originalImplementation.signInPOST!(input) }, } }, @@ -1116,10 +1172,6 @@ async function validateAndGetUserInfoFromExternalProvider(email: string, passwor 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.usermetadata.asyncio import ( - get_user_metadata, - update_user_metadata, -) from supertokens_python.recipe.emailverification.asyncio import ( create_email_verification_token, verify_email_using_token, @@ -1130,23 +1182,46 @@ from supertokens_python.recipe.emailpassword.asyncio import ( update_email_or_password, ) from supertokens_python.recipe.emailpassword.interfaces import ( - RecipeInterface, - SignInOkResult, - SignInWrongCredentialsError, SignUpEmailAlreadyExistsError, ) from supertokens_python.recipe.emailverification.interfaces import ( CreateEmailVerificationTokenOkResult, ) -from typing import Dict, Any, Union +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import emailpassword +from supertokens_python.recipe.emailpassword.interfaces import ( + APIInterface, + SignInPostOkResult, + SignInPostWrongCredentialsError, + APIOptions, +) +from supertokens_python.types import GeneralErrorResponse +from supertokens_python.recipe.emailpassword.types import FormField +from typing import Dict, Any, Union, List +from supertokens_python.recipe.usermetadata.asyncio import ( + get_user_metadata, + update_user_metadata, +) -def override_emailpassword_functions(original_implementation: RecipeInterface): - original_emailpassword_sign_in = original_implementation.sign_in +def override_emailpassword_apis(original_implementation: APIInterface): + original_emailpassword_sign_in = original_implementation.sign_in_post - async def emailpassword_sign_in( - email: str, password: str, tenant_id: str, user_context: Dict[str, Any] - ) -> Union[SignInOkResult, SignInWrongCredentialsError]: + async def sign_in( + form_fields: List[FormField], + tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], + ) -> Union[ + SignInPostOkResult, SignInPostWrongCredentialsError, GeneralErrorResponse + ]: + 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 emailpassword_user = await get_user_by_email(tenant_id, email, user_context) @@ -1158,7 +1233,7 @@ def override_emailpassword_functions(original_implementation: RecipeInterface): ) if legacy_user_info is None: # Credentials are incorrect - return SignInWrongCredentialsError() + return SignInPostWrongCredentialsError() # Call the sign_up function to create a new SuperTokens user. response = await sign_up(email, password, tenant_id, user_context) @@ -1190,9 +1265,10 @@ def override_emailpassword_functions(original_implementation: RecipeInterface): user_context, ) - return SignInOkResult(response.user) - # highlight-start + emailpassword_user = await get_user_by_email(tenant_id, email, user_context) + if emailpassword_user is None: + raise Exception("Should never come here") # Check if the user signing in has a temporary password metadata_result = await get_user_metadata(emailpassword_user.user_id) if ( @@ -1217,15 +1293,15 @@ def override_emailpassword_functions(original_implementation: RecipeInterface): await update_user_metadata( emailpassword_user.user_id, {"isUsingTemporaryPassword": None} ) - return SignInOkResult(emailpassword_user) - return SignInWrongCredentialsError() + else: + return SignInPostWrongCredentialsError() # highlight-end return await original_emailpassword_sign_in( - email, password, tenant_id, user_context + form_fields, tenant_id, api_options, user_context ) - original_implementation.sign_in = emailpassword_sign_in + original_implementation.sign_in_post = sign_in return original_implementation @@ -1246,9 +1322,7 @@ init( framework="...", # type: ignore recipe_list=[ emailpassword.init( - override=emailpassword.InputOverrideConfig( - functions=override_emailpassword_functions - ) + override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ) ], ) @@ -1274,18 +1348,28 @@ func main() { RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ - Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { + APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // Copy the original implementation of the signIn function - originalSignIn := *originalImplementation.SignIn + originalSignIn := *originalImplementation.SignInPOST // Override the function - (*originalImplementation.SignIn) = func(email, password, tenantId string, userContext supertokens.UserContext) (epmodels.SignInResponse, error) { + (*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" { + email = formField.Value + } + if formField.ID == "password" { + password = formField.Value + } + } // Check if an email-password user with the input email exists in SuperTokens emailPasswordUser, err := emailpassword.GetUserByEmail(tenantId, email) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } if emailPasswordUser == nil { @@ -1294,7 +1378,7 @@ func main() { legacyUserInfo := validateAndGetUserInfoFromExternalProvider(email, password) if legacyUserInfo == nil { - return epmodels.SignInResponse{ + return epmodels.SignInPOSTResponse{ WrongCredentialsError: &struct{}{}, }, nil } @@ -1302,11 +1386,11 @@ func main() { // Call the email-password signup function to create a new SuperTokens user. response, err := emailpassword.SignUp(tenantId, email, password) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } if response.OK == nil { - return epmodels.SignInResponse{}, errors.New("Should never come here") + return epmodels.SignInPOSTResponse{}, errors.New("Should never come here") } // Map the external provider's userId to the SuperTokens userId @@ -1319,29 +1403,26 @@ func main() { // Generate an email verification token for the user generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, response.OK.User.ID, &email) if err != nil { - return epmodels.SignInResponse{}, err + 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.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } } } - - return epmodels.SignInResponse{ - OK: &struct{ User epmodels.User }{response.OK.User}, - }, nil - } + emailPasswordUser, err = emailpassword.GetUserByEmail(tenantId, email) + // Check if the user signing in has a temporary password metadata, err := usermetadata.GetUserMetadata(emailPasswordUser.ID) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } isUsingTemporaryPassword, ok := metadata["isUsingTemporaryPassword"] @@ -1354,7 +1435,7 @@ func main() { usePasswordPolicy := false _, err = emailpassword.UpdateEmailOrPassword(emailPasswordUser.ID, nil, &password, &usePasswordPolicy, &tenantId) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } // Update the user's metadata to remove the isUsingTemporaryPassword flag @@ -1362,20 +1443,16 @@ func main() { "isUsingTemporaryPassword": nil, }) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } - - return epmodels.SignInResponse{ - OK: &struct{ User epmodels.User }{*emailPasswordUser}, + } else { + return epmodels.SignInPOSTResponse{ + WrongCredentialsError: &struct{}{}, }, nil } - - return epmodels.SignInResponse{ - WrongCredentialsError: &struct{}{}, - }, nil } - return originalSignIn(email, password, tenantId, userContext) + return originalSignIn(formFields, tenantId, options, userContext) } return originalImplementation @@ -1402,7 +1479,7 @@ func validateAndGetUserInfoFromExternalProvider(email string, password string) * -The code above adds the following changes to the `signIn` function: +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. diff --git a/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx b/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx index 1c20f5f40..2ab9d98b3 100644 --- a/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx +++ b/v2/thirdpartyemailpassword/migration/account-creation/ep-migration-without-password-hash.mdx @@ -36,7 +36,7 @@ We will need to make the following customizations to SuperTokens authentication ## Step 1) Prevent signups from users who exist in the external provider -To implement this change we will override the function that handles email-password login when initializing the `ThirdPartyEmailPassword` recipe on the backend. +To implement this change we will override the api that handles email-password login when initializing the recipe on the backend. @@ -46,18 +46,19 @@ import EmailPassword from "supertokens-node/recipe/emailpassword" EmailPassword.init({ override: { - functions: (originalImplementation) => { + apis: (originalImplementation) => { return { ...originalImplementation, - signUp: async function(input) { + signUpPOST: async function (input) { + let email = input.formFields.find((field) => field.id === "email")!.value; // Check if the user signing in exists in the external provider - if(await doesUserExistInExternalProvider(input.email)){ + 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.signUp(input); + return originalImplementation.signUpPOST!(input); }, } }, @@ -77,22 +78,40 @@ async function doesUserExistInExternalProvider(email: string): Promise ```python from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword -from supertokens_python.recipe.emailpassword.interfaces import RecipeInterface, SignUpOkResult, SignUpEmailAlreadyExistsError -from typing import Dict, Any, Union +from supertokens_python.recipe.emailpassword.interfaces import ( + APIInterface, + SignUpPostOkResult, + SignUpPostEmailAlreadyExistsError, + APIOptions, +) +from supertokens_python.types import GeneralErrorResponse +from supertokens_python.recipe.emailpassword.types import FormField +from typing import Dict, Any, Union, List -def override_email_password_functions(original_implementation: RecipeInterface): - original_sign_up = original_implementation.sign_up +def override_email_password_apis(original_implementation: APIInterface): + original_sign_up = original_implementation.sign_up_post - async def sign_up(email: str, password: str, tenant_id: str, user_context: Dict[str, Any]) -> Union[SignUpOkResult, SignUpEmailAlreadyExistsError]: + async def sign_up( + form_fields: List[FormField], + tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], + ) -> Union[ + SignUpPostOkResult, SignUpPostEmailAlreadyExistsError, GeneralErrorResponse + ]: + 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 SignUpEmailAlreadyExistsError() + return SignUpPostEmailAlreadyExistsError() - return await original_sign_up(email, password, tenant_id, user_context) + return await original_sign_up(form_fields, tenant_id, api_options, user_context) - original_implementation.sign_up = sign_up + original_implementation.sign_up_post = sign_up return original_implementation @@ -102,16 +121,15 @@ async def does_user_exist_in_external_provider(email: str): init( - app_info=InputAppInfo( - api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework="...", # type: ignore recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( - functions=override_email_password_functions, + apis=override_email_password_apis, ) ) - ] + ], ) ``` @@ -131,21 +149,27 @@ func main() { RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ - Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { + APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // Copy the original implementation of the signUp function - originalSignUp := *originalImplementation.SignUp + originalSignUp := *originalImplementation.SignUpPOST // Override the signUp function - (*originalImplementation.SignUp) = func(email, password string, tenantId string, userContext supertokens.UserContext) (epmodels.SignUpResponse, error) { + (*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" { + email = formField.Value + } + } // 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.SignUpResponse{ + return epmodels.SignUpPOSTResponse{ EmailAlreadyExistsError: &struct{}{}, }, nil } - return originalSignUp(email, password, tenantId, userContext) + return originalSignUp(formFields, tenantId, options, userContext) } return originalImplementation }, @@ -165,11 +189,11 @@ func doesUserExistInExternalProvider(email string) bool { -We modify the `signUp` function 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` +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 provider -To implement this flow we will override the function that handles email-password login when initializing the `ThirdPartyEmailPassword` recipe on the backend. +To implement this flow we will override the API that handles email-password login when initializing the recipe on the backend. @@ -182,22 +206,24 @@ import { RecipeUserId } from "supertokens-node"; EmailPassword.init({ override: { - functions: (originalImplementation) => { + apis: (originalImplementation) => { return { ...originalImplementation, - signIn: async function (input) { + 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; + let password = input.formFields.find((field) => field.id === "password")!.value; let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { - email: input.email + email: email }, undefined, input.userContext); let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { - return u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "emailpassword") !== undefined; + 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(input.email, input.password) + let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password) if (legacyUserInfo === undefined) { // credentials are incorrect return { @@ -206,14 +232,14 @@ EmailPassword.init({ } // Call the signup function to create a new SuperTokens user. - let signUpResponse = await EmailPassword.signUp(input.tenantId, input.email, input.password, undefined, input.userContext); + 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); @@ -221,18 +247,16 @@ EmailPassword.init({ // 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, input.email, input.userContext); + 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 signUpResponse; } - return originalImplementation.signIn(input) + return originalImplementation.signInPOST!(input) }, } }, @@ -256,17 +280,48 @@ async function validateAndGetUserInfoFromExternalProvider(email: string, passwor 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.emailverification.asyncio import ( + create_email_verification_token, + verify_email_using_token, +) from supertokens_python.recipe.emailpassword.asyncio import get_user_by_email, sign_up -from supertokens_python.recipe.emailpassword.interfaces import RecipeInterface, SignInOkResult, SignInWrongCredentialsError, SignUpEmailAlreadyExistsError -from supertokens_python.recipe.emailverification.interfaces import CreateEmailVerificationTokenOkResult -from typing import Dict, Any, Union +from supertokens_python.recipe.emailpassword.interfaces import ( + SignUpEmailAlreadyExistsError, +) +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, + SignInPostOkResult, + SignInPostWrongCredentialsError, + APIOptions, +) +from supertokens_python.types import GeneralErrorResponse +from supertokens_python.recipe.emailpassword.types import FormField +from typing import Dict, Any, Union, List -def override_emailpassword_functions(original_implementation: RecipeInterface): - original_emailpassword_sign_in = original_implementation.sign_in +def override_emailpassword_apis(original_implementation: APIInterface): + original_emailpassword_sign_in = original_implementation.sign_in_post - async def sign_in(email: str, password: str, tenant_id: str, user_context: Dict[str, Any]) -> Union[SignInOkResult, SignInWrongCredentialsError]: + async def sign_in( + form_fields: List[FormField], + tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], + ) -> Union[ + SignInPostOkResult, SignInPostWrongCredentialsError, GeneralErrorResponse + ]: + 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 emailpassword_user = await get_user_by_email(tenant_id, email, user_context) @@ -274,10 +329,11 @@ def override_emailpassword_functions(original_implementation: RecipeInterface): # 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) + email, password + ) if legacy_user_info is None: # Credentials are incorrect - return SignInWrongCredentialsError() + return SignInPostWrongCredentialsError() # Call the sign_up function to create a new SuperTokens user. response = await sign_up(email, password, tenant_id, user_context) @@ -285,32 +341,38 @@ def override_emailpassword_functions(original_implementation: RecipeInterface): raise Exception("Should never come here") # Map the external provider's userId to the SuperTokens userId - await create_user_id_mapping(response.user.user_id, - legacy_user_info.user_id) + await create_user_id_mapping( + response.user.user_id, legacy_user_info.user_id + ) # Set the userId in the response to use the provider's userId response.user.user_id = 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.user.user_id, - email, - user_context, + generate_email_verification_response = ( + await create_email_verification_token( + tenant_id, + response.user.user_id, + email, + user_context, + ) ) - if isinstance(generate_email_verification_response, CreateEmailVerificationTokenOkResult): + if isinstance( + generate_email_verification_response, + CreateEmailVerificationTokenOkResult, + ): await verify_email_using_token( tenant_id, - generate_email_verification_response.token, + generate_email_verification_response.token, user_context, ) - return SignInOkResult(response.user) - - return await original_emailpassword_sign_in(email, password, tenant_id, user_context) + return await original_emailpassword_sign_in( + form_fields, tenant_id, api_options, user_context + ) - original_implementation.sign_in = sign_in + original_implementation.sign_in_post = sign_in return original_implementation @@ -320,21 +382,20 @@ class ExternalUserInfo: self.isEmailVerified: bool = isEmailVerified -async def validate_and_get_user_info_from_external_provider(email: str, password: str) -> Union[None, ExternalUserInfo]: +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='...', # type: ignore + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework="...", # type: ignore recipe_list=[ emailpassword.init( - override=emailpassword.InputOverrideConfig( - functions=override_emailpassword_functions - ) + override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ) - ] + ], ) ``` @@ -357,18 +418,28 @@ func main() { RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ - Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { + APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // Copy the original implementation of the signIn function - originalSignIn := *originalImplementation.SignIn + originalSignIn := *originalImplementation.SignInPOST // Override the function - (*originalImplementation.SignIn) = func(email, password, tenantId string, userContext supertokens.UserContext) (epmodels.SignInResponse, error) { + (*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" { + email = formField.Value + } + if formField.ID == "password" { + password = formField.Value + } + } // Check if an email-password user with the input email exists in SuperTokens emailPasswordUser, err := emailpassword.GetUserByEmail(tenantId, email) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } if emailPasswordUser == nil { @@ -377,7 +448,7 @@ func main() { legacyUserInfo := validateAndGetUserInfoFromExternalProvider(email, password) if legacyUserInfo == nil { - return epmodels.SignInResponse{ + return epmodels.SignInPOSTResponse{ WrongCredentialsError: &struct{}{}, }, nil } @@ -385,17 +456,17 @@ func main() { // Call the email-password signup function to create a new SuperTokens user. response, err := emailpassword.SignUp(tenantId, email, password) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } if response.OK == nil { - return epmodels.SignInResponse{}, errors.New("Should never come here") + 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.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } // Set the userId in the response to use the provider's userId response.OK.User.ID = legacyUserInfo.userId @@ -405,7 +476,7 @@ func main() { // Generate an email verification token for the user generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, response.OK.User.ID, &email) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } if generateEmailVerificationTokenResponse.OK != nil { @@ -413,14 +484,9 @@ func main() { emailverification.VerifyEmailUsingToken(tenantId, generateEmailVerificationTokenResponse.OK.Token) } } - - return epmodels.SignInResponse{ - OK: &struct{ User epmodels.User }{response.OK.User}, - }, nil - } - return originalSignIn(email, password, tenantId, userContext) + return originalSignIn(formFields, tenantId, options, userContext) } return originalImplementation @@ -446,7 +512,7 @@ func validateAndGetUserInfoFromExternalProvider(email string, password string) * -The code above overrides the `signIn` function with the following changes to achieve "**just in time**" migration: +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`. @@ -471,15 +537,10 @@ import UserMetadata from "supertokens-node/recipe/usermetadata" EmailPassword.init({ override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - // TODO: implementation details in previous step - } - }, 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; @@ -828,12 +889,6 @@ import UserMetadata from "supertokens-node/recipe/usermetadata" EmailPassword.init({ override: { - functions: (originalImplementaion) => { - return { - ...originalImplementaion - // TODO: implentation details in previous step - } - }, apis: (originalImplementation) => { return { ...originalImplementation, @@ -1015,22 +1070,24 @@ import { RecipeUserId } from "supertokens-node"; EmailPassword.init({ override: { - functions: (originalImplementation) => { + apis: (originalImplementation) => { return { ...originalImplementation, - signIn: async function (input) { + 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; + let password = input.formFields.find((field) => field.id === "password")!.value; let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { - email: input.email + email: email }, undefined, input.userContext); let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { - return u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "emailpassword") !== undefined; + 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(input.email, input.password) + let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password) if (legacyUserInfo === undefined) { // credentials are incorrect return { @@ -1039,7 +1096,7 @@ EmailPassword.init({ } // Call the signup function to create a new SuperTokens user. - let signUpResponse = await EmailPassword.signUp(input.tenantId, input.email, input.password, undefined, input.userContext); + let signUpResponse = await EmailPassword.signUp(input.tenantId, email, password, undefined, input.userContext); if (signUpResponse.status !== "OK") { throw new Error("Should never come here") } @@ -1053,47 +1110,46 @@ EmailPassword.init({ // 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, input.email, input.userContext); + 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 signUpResponse; } // highlight-start + emailPasswordUser = supertokensUsersWithSameEmail.find(u => { + return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined; + }) + if (emailPasswordUser === undefined) { + throw new Error("Should never come here") + } // 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(input.email, input.password); + let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password); if (legacyUserInfo) { - let loginMethod = emailPasswordUser.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(input.email)); + 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: input.password, + password: password, applyPasswordPolicy: false }) // Update the user's metadata to remove the isUsingTemporaryPassword flag UserMetadata.updateUserMetadata(emailPasswordUser.id, { isUsingTemporaryPassword: null }) - + } else { return { - status: "OK", - user: emailPasswordUser, - recipeUserId: loginMethod!.recipeUserId + status: "WRONG_CREDENTIALS_ERROR" } } - return { - status: "WRONG_CREDENTIALS_ERROR" - } } // highlight-end - return originalImplementation.signIn(input) + return originalImplementation.signInPOST!(input) }, } }, @@ -1116,10 +1172,6 @@ async function validateAndGetUserInfoFromExternalProvider(email: string, passwor 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.usermetadata.asyncio import ( - get_user_metadata, - update_user_metadata, -) from supertokens_python.recipe.emailverification.asyncio import ( create_email_verification_token, verify_email_using_token, @@ -1130,23 +1182,46 @@ from supertokens_python.recipe.emailpassword.asyncio import ( update_email_or_password, ) from supertokens_python.recipe.emailpassword.interfaces import ( - RecipeInterface, - SignInOkResult, - SignInWrongCredentialsError, SignUpEmailAlreadyExistsError, ) from supertokens_python.recipe.emailverification.interfaces import ( CreateEmailVerificationTokenOkResult, ) -from typing import Dict, Any, Union +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import emailpassword +from supertokens_python.recipe.emailpassword.interfaces import ( + APIInterface, + SignInPostOkResult, + SignInPostWrongCredentialsError, + APIOptions, +) +from supertokens_python.types import GeneralErrorResponse +from supertokens_python.recipe.emailpassword.types import FormField +from typing import Dict, Any, Union, List +from supertokens_python.recipe.usermetadata.asyncio import ( + get_user_metadata, + update_user_metadata, +) -def override_emailpassword_functions(original_implementation: RecipeInterface): - original_emailpassword_sign_in = original_implementation.sign_in +def override_emailpassword_apis(original_implementation: APIInterface): + original_emailpassword_sign_in = original_implementation.sign_in_post - async def emailpassword_sign_in( - email: str, password: str, tenant_id: str, user_context: Dict[str, Any] - ) -> Union[SignInOkResult, SignInWrongCredentialsError]: + async def sign_in( + form_fields: List[FormField], + tenant_id: str, + api_options: APIOptions, + user_context: Dict[str, Any], + ) -> Union[ + SignInPostOkResult, SignInPostWrongCredentialsError, GeneralErrorResponse + ]: + 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 emailpassword_user = await get_user_by_email(tenant_id, email, user_context) @@ -1158,7 +1233,7 @@ def override_emailpassword_functions(original_implementation: RecipeInterface): ) if legacy_user_info is None: # Credentials are incorrect - return SignInWrongCredentialsError() + return SignInPostWrongCredentialsError() # Call the sign_up function to create a new SuperTokens user. response = await sign_up(email, password, tenant_id, user_context) @@ -1190,9 +1265,10 @@ def override_emailpassword_functions(original_implementation: RecipeInterface): user_context, ) - return SignInOkResult(response.user) - # highlight-start + emailpassword_user = await get_user_by_email(tenant_id, email, user_context) + if emailpassword_user is None: + raise Exception("Should never come here") # Check if the user signing in has a temporary password metadata_result = await get_user_metadata(emailpassword_user.user_id) if ( @@ -1217,15 +1293,15 @@ def override_emailpassword_functions(original_implementation: RecipeInterface): await update_user_metadata( emailpassword_user.user_id, {"isUsingTemporaryPassword": None} ) - return SignInOkResult(emailpassword_user) - return SignInWrongCredentialsError() + else: + return SignInPostWrongCredentialsError() # highlight-end return await original_emailpassword_sign_in( - email, password, tenant_id, user_context + form_fields, tenant_id, api_options, user_context ) - original_implementation.sign_in = emailpassword_sign_in + original_implementation.sign_in_post = sign_in return original_implementation @@ -1246,9 +1322,7 @@ init( framework="...", # type: ignore recipe_list=[ emailpassword.init( - override=emailpassword.InputOverrideConfig( - functions=override_emailpassword_functions - ) + override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ) ], ) @@ -1274,18 +1348,28 @@ func main() { RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ - Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { + APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // Copy the original implementation of the signIn function - originalSignIn := *originalImplementation.SignIn + originalSignIn := *originalImplementation.SignInPOST // Override the function - (*originalImplementation.SignIn) = func(email, password, tenantId string, userContext supertokens.UserContext) (epmodels.SignInResponse, error) { + (*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" { + email = formField.Value + } + if formField.ID == "password" { + password = formField.Value + } + } // Check if an email-password user with the input email exists in SuperTokens emailPasswordUser, err := emailpassword.GetUserByEmail(tenantId, email) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } if emailPasswordUser == nil { @@ -1294,7 +1378,7 @@ func main() { legacyUserInfo := validateAndGetUserInfoFromExternalProvider(email, password) if legacyUserInfo == nil { - return epmodels.SignInResponse{ + return epmodels.SignInPOSTResponse{ WrongCredentialsError: &struct{}{}, }, nil } @@ -1302,11 +1386,11 @@ func main() { // Call the email-password signup function to create a new SuperTokens user. response, err := emailpassword.SignUp(tenantId, email, password) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } if response.OK == nil { - return epmodels.SignInResponse{}, errors.New("Should never come here") + return epmodels.SignInPOSTResponse{}, errors.New("Should never come here") } // Map the external provider's userId to the SuperTokens userId @@ -1319,29 +1403,26 @@ func main() { // Generate an email verification token for the user generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, response.OK.User.ID, &email) if err != nil { - return epmodels.SignInResponse{}, err + 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.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } } } - - return epmodels.SignInResponse{ - OK: &struct{ User epmodels.User }{response.OK.User}, - }, nil - } + emailPasswordUser, err = emailpassword.GetUserByEmail(tenantId, email) + // Check if the user signing in has a temporary password metadata, err := usermetadata.GetUserMetadata(emailPasswordUser.ID) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } isUsingTemporaryPassword, ok := metadata["isUsingTemporaryPassword"] @@ -1354,7 +1435,7 @@ func main() { usePasswordPolicy := false _, err = emailpassword.UpdateEmailOrPassword(emailPasswordUser.ID, nil, &password, &usePasswordPolicy, &tenantId) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } // Update the user's metadata to remove the isUsingTemporaryPassword flag @@ -1362,20 +1443,16 @@ func main() { "isUsingTemporaryPassword": nil, }) if err != nil { - return epmodels.SignInResponse{}, err + return epmodels.SignInPOSTResponse{}, err } - - return epmodels.SignInResponse{ - OK: &struct{ User epmodels.User }{*emailPasswordUser}, + } else { + return epmodels.SignInPOSTResponse{ + WrongCredentialsError: &struct{}{}, }, nil } - - return epmodels.SignInResponse{ - WrongCredentialsError: &struct{}{}, - }, nil } - return originalSignIn(email, password, tenantId, userContext) + return originalSignIn(formFields, tenantId, options, userContext) } return originalImplementation @@ -1402,7 +1479,7 @@ func validateAndGetUserInfoFromExternalProvider(email string, password string) * -The code above adds the following changes to the `signIn` function: +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.