diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index bf6de6dfb..94c0f5211 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -34,7 +34,7 @@ highlighting the necessary changes) - To know which one it is, run find the latest released tag (`git tag`) in the format `vX.Y.Z`, and then find the latest branch (`git branch --all`) whose `X.Y` is greater than the latest released tag. - If no such branch exists, then create one from the latest released branch. - +- [ ] If added a foreign key constraint on `app_id_to_user_id` table, make sure to delete from this table when deleting the user as well if `deleteUserIdMappingToo` is false. ## Remaining TODOs for this PR - [ ] Item1 diff --git a/CHANGELOG.md b/CHANGELOG.md index ab165dc52..562876542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,187 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [unreleased] +## [7.0.0] - 2023-09-19 + +- Support for CDI version 4.0 +- Adds Account Linking feature + +### Session recipe changes + +- New access token version: v5, which contains a required prop: `rsub`. This contains the recipe user ID that belongs to the login method that the user used to login. The `sub` claim in the access token payload is now the primary user ID. +- APIs that return `SessionInformation` (like GET `/recipe/session`) contains userId, recipeUserId in the response. +- Apis that create / modify / refresh a session return the `recipeUserId` in the `session` object in the response. +- Token theft detected response returns userId and recipeUserId + +### Db Schema changes + +- Adds columns `primary_or_recipe_user_id`, `is_linked_or_is_a_primary_user` and `primary_or_recipe_user_time_joined` to `all_auth_recipe_users` table +- Adds columns `primary_or_recipe_user_id` and `is_linked_or_is_a_primary_user` to `app_id_to_user_id` table +- Removes index `all_auth_recipe_users_pagination_index` and addes `all_auth_recipe_users_pagination_index1`, + `all_auth_recipe_users_pagination_index2`, `all_auth_recipe_users_pagination_index3` and + `all_auth_recipe_users_pagination_index4` indexes instead on `all_auth_recipe_users` table +- Adds `all_auth_recipe_users_recipe_id_index` on `all_auth_recipe_users` table +- Adds `all_auth_recipe_users_primary_user_id_index` on `all_auth_recipe_users` table +- Adds `email` column to `emailpassword_pswd_reset_tokens` table +- Changes `user_id` foreign key constraint on `emailpassword_pswd_reset_tokens` to `app_id_to_user_id` table + +### Migration steps for SQL + +1. Ensure that the core is already upgraded to version 6.0.13 (CDI version 3.0) +2. Stop the core instance(s) +3. Run the migration script + +
+ + If using PostgreSQL + + ```sql + ALTER TABLE all_auth_recipe_users + ADD COLUMN primary_or_recipe_user_id CHAR(36) NOT NULL DEFAULT ('0'); + + ALTER TABLE all_auth_recipe_users + ADD COLUMN is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE; + + ALTER TABLE all_auth_recipe_users + ADD COLUMN primary_or_recipe_user_time_joined BIGINT NOT NULL DEFAULT 0; + + UPDATE all_auth_recipe_users + SET primary_or_recipe_user_id = user_id + WHERE primary_or_recipe_user_id = '0'; + + UPDATE all_auth_recipe_users + SET primary_or_recipe_user_time_joined = time_joined + WHERE primary_or_recipe_user_time_joined = 0; + + ALTER TABLE all_auth_recipe_users + ADD CONSTRAINT all_auth_recipe_users_primary_or_recipe_user_id_fkey + FOREIGN KEY (app_id, primary_or_recipe_user_id) + REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + + ALTER TABLE all_auth_recipe_users + ALTER primary_or_recipe_user_id DROP DEFAULT; + + ALTER TABLE app_id_to_user_id + ADD COLUMN primary_or_recipe_user_id CHAR(36) NOT NULL DEFAULT ('0'); + + ALTER TABLE app_id_to_user_id + ADD COLUMN is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE; + + UPDATE app_id_to_user_id + SET primary_or_recipe_user_id = user_id + WHERE primary_or_recipe_user_id = '0'; + + ALTER TABLE app_id_to_user_id + ADD CONSTRAINT app_id_to_user_id_primary_or_recipe_user_id_fkey + FOREIGN KEY (app_id, primary_or_recipe_user_id) + REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + + ALTER TABLE app_id_to_user_id + ALTER primary_or_recipe_user_id DROP DEFAULT; + + DROP INDEX all_auth_recipe_users_pagination_index; + + CREATE INDEX all_auth_recipe_users_pagination_index1 ON all_auth_recipe_users ( + app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_pagination_index2 ON all_auth_recipe_users ( + app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_pagination_index3 ON all_auth_recipe_users ( + recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_pagination_index4 ON all_auth_recipe_users ( + recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_primary_user_id_index ON all_auth_recipe_users (primary_or_recipe_user_id, app_id); + + CREATE INDEX all_auth_recipe_users_recipe_id_index ON all_auth_recipe_users (app_id, recipe_id, tenant_id); + + ALTER TABLE emailpassword_pswd_reset_tokens DROP CONSTRAINT IF EXISTS emailpassword_pswd_reset_tokens_user_id_fkey; + + ALTER TABLE emailpassword_pswd_reset_tokens ADD CONSTRAINT emailpassword_pswd_reset_tokens_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + + ALTER TABLE emailpassword_pswd_reset_tokens ADD COLUMN email VARCHAR(256); + ``` +
+ +
+ + If using MySQL + + ```sql + ALTER TABLE all_auth_recipe_users + ADD primary_or_recipe_user_id CHAR(36) NOT NULL DEFAULT ('0'); + + ALTER TABLE all_auth_recipe_users + ADD is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE; + + ALTER TABLE all_auth_recipe_users + ADD primary_or_recipe_user_time_joined BIGINT UNSIGNED NOT NULL DEFAULT 0; + + UPDATE all_auth_recipe_users + SET primary_or_recipe_user_id = user_id + WHERE primary_or_recipe_user_id = '0'; + + UPDATE all_auth_recipe_users + SET primary_or_recipe_user_time_joined = time_joined + WHERE primary_or_recipe_user_time_joined = 0; + + ALTER TABLE all_auth_recipe_users + ADD FOREIGN KEY (app_id, primary_or_recipe_user_id) + REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + + ALTER TABLE all_auth_recipe_users + ALTER primary_or_recipe_user_id DROP DEFAULT; + + ALTER TABLE app_id_to_user_id + ADD primary_or_recipe_user_id CHAR(36) NOT NULL DEFAULT ('0'); + + ALTER TABLE app_id_to_user_id + ADD is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE; + + UPDATE app_id_to_user_id + SET primary_or_recipe_user_id = user_id + WHERE primary_or_recipe_user_id = '0'; + + ALTER TABLE app_id_to_user_id + ADD FOREIGN KEY (app_id, primary_or_recipe_user_id) + REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + + ALTER TABLE app_id_to_user_id + ALTER primary_or_recipe_user_id DROP DEFAULT; + + DROP INDEX all_auth_recipe_users_pagination_index ON all_auth_recipe_users; + + CREATE INDEX all_auth_recipe_users_pagination_index1 ON all_auth_recipe_users ( + app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_pagination_index2 ON all_auth_recipe_users ( + app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_pagination_index3 ON all_auth_recipe_users ( + recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_pagination_index4 ON all_auth_recipe_users ( + recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); + + CREATE INDEX all_auth_recipe_users_primary_user_id_index ON all_auth_recipe_users (primary_or_recipe_user_id, app_id); + + CREATE INDEX all_auth_recipe_users_recipe_id_index ON all_auth_recipe_users (app_id, recipe_id, tenant_id); + + ALTER TABLE emailpassword_pswd_reset_tokens + DROP FOREIGN KEY emailpassword_pswd_reset_tokens_ibfk_1; + + ALTER TABLE emailpassword_pswd_reset_tokens + ADD FOREIGN KEY (app_id, user_id) REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE; + + ALTER TABLE emailpassword_pswd_reset_tokens ADD email VARCHAR(256); + ``` + +
+ +4. Start the new instance(s) of the core (version 7.0.0) + ## [6.0.13] - 2023-09-15 - Fixes paid stats reporting for multitenancy @@ -86,8 +267,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Support for multitenancy. - New config `supertokens_saas_secret` added to support multitenancy in SaaS mode. -- New config `supertokens_default_cdi_version` is added to specify the version of CDI core must assume when the - version is not specified in the request. If this config is not specified, the core will assume the latest version. +- New config `supertokens_default_cdi_version` is added to specify the version of CDI core must assume when the version + is not specified in the request. If this config is not specified, the core will assume the latest version. ### Fixes @@ -1811,7 +1992,6 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). "key_string": "$keys.value", "algorithm": "RS256", "created_at": "$keys.created_at_time", - } }, { @@ -1826,7 +2006,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). } } - ]); + ]); ``` - If using `access_token_signing_key_dynamic` true or not set: diff --git a/build.gradle b/build.gradle index 5ae32374e..a2c2d397a 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" } // } //} -version = "6.0.13" +version = "7.0.0" repositories { diff --git a/cli/jar/cli.jar b/cli/jar/cli.jar index a5677b8fa..e34e8ae49 100644 Binary files a/cli/jar/cli.jar and b/cli/jar/cli.jar differ diff --git a/coreDriverInterfaceSupported.json b/coreDriverInterfaceSupported.json index 1b1bc74f9..0c9d09fe0 100644 --- a/coreDriverInterfaceSupported.json +++ b/coreDriverInterfaceSupported.json @@ -16,6 +16,7 @@ "2.19", "2.20", "2.21", - "3.0" + "3.0", + "4.0" ] } \ No newline at end of file diff --git a/downloader/jar/downloader.jar b/downloader/jar/downloader.jar index 72dfc6701..7a4eb7a87 100644 Binary files a/downloader/jar/downloader.jar and b/downloader/jar/downloader.jar differ diff --git a/ee/jar/ee.jar b/ee/jar/ee.jar index 38b5ff04a..11e6be2a9 100644 Binary files a/ee/jar/ee.jar and b/ee/jar/ee.jar differ diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 56f02d526..b68622959 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -272,6 +272,49 @@ private JsonObject getMultiTenancyStats() return stats; } + private JsonObject getAccountLinkingStats() throws StorageQueryException { + JsonObject result = new JsonObject(); + Storage[] storages = StorageLayer.getStoragesForApp(main, this.appIdentifier); + boolean usesAccountLinking = false; + + for (Storage storage : storages) { + if (((AuthRecipeStorage)storage).checkIfUsesAccountLinking(this.appIdentifier)) { + usesAccountLinking = true; + break; + } + } + + result.addProperty("usesAccountLinking", usesAccountLinking); + if (!usesAccountLinking) { + result.addProperty("totalUserCountWithMoreThanOneLoginMethod", 0); + JsonArray mauArray = new JsonArray(); + for (int i = 0; i < 30; i++) { + mauArray.add(new JsonPrimitive(0)); + } + result.add("mauWithMoreThanOneLoginMethod", mauArray); + return result; + } + + int totalUserCountWithMoreThanOneLoginMethod = 0; + int[] maus = new int[30]; + + long now = System.currentTimeMillis(); + long today = now - (now % (24 * 60 * 60 * 1000L)); + + for (Storage storage : storages) { + totalUserCountWithMoreThanOneLoginMethod += ((AuthRecipeStorage)storage).getUsersCountWithMoreThanOneLoginMethod(this.appIdentifier); + + for (int i = 0; i < 30; i++) { + long timestamp = today - (i * 24 * 60 * 60 * 1000L); + maus[i] += ((ActiveUsersStorage)storage).countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(appIdentifier, timestamp); + } + } + + result.addProperty("totalUserCountWithMoreThanOneLoginMethod", totalUserCountWithMoreThanOneLoginMethod); + result.add("mauWithMoreThanOneLoginMethod", new Gson().toJsonTree(maus)); + return result; + } + private JsonArray getMAUs() throws StorageQueryException, TenantOrAppNotFoundException { JsonArray mauArr = new JsonArray(); for (int i = 0; i < 30; i++) { @@ -319,6 +362,10 @@ public JsonObject getPaidFeatureStats() throws StorageQueryException, TenantOrAp if (feature == EE_FEATURES.MULTI_TENANCY) { usageStats.add(EE_FEATURES.MULTI_TENANCY.toString(), getMultiTenancyStats()); } + + if (feature == EE_FEATURES.ACCOUNT_LINKING) { + usageStats.add(EE_FEATURES.ACCOUNT_LINKING.toString(), getAccountLinkingStats()); + } } usageStats.add("maus", getMAUs()); diff --git a/jar/core-6.0.13.jar b/jar/core-7.0.0.jar similarity index 52% rename from jar/core-6.0.13.jar rename to jar/core-7.0.0.jar index d7483dd77..723342c67 100644 Binary files a/jar/core-6.0.13.jar and b/jar/core-7.0.0.jar differ diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index 431c3b083..a5fdc62cd 100644 --- a/pluginInterfaceSupported.json +++ b/pluginInterfaceSupported.json @@ -1,6 +1,6 @@ { "_comment": "contains a list of plugin interfaces branch names that this core supports", "versions": [ - "3.0" + "4.0" ] } \ No newline at end of file diff --git a/src/main/java/io/supertokens/ActiveUsers.java b/src/main/java/io/supertokens/ActiveUsers.java index 55224cb49..7c4601958 100644 --- a/src/main/java/io/supertokens/ActiveUsers.java +++ b/src/main/java/io/supertokens/ActiveUsers.java @@ -1,6 +1,8 @@ package io.supertokens; +import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; @@ -37,4 +39,18 @@ public static int countUsersActiveSince(Main main, long time) return countUsersActiveSince(new AppIdentifierWithStorage(null, null, StorageLayer.getStorage(main)), main, time); } + + public static void removeActiveUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId) + throws StorageQueryException { + try { + ((AuthRecipeSQLStorage) appIdentifierWithStorage.getActiveUsersStorage()).startTransaction(con -> { + appIdentifierWithStorage.getActiveUsersStorage().deleteUserActive_Transaction(con, appIdentifierWithStorage, userId); + ((AuthRecipeSQLStorage) appIdentifierWithStorage.getActiveUsersStorage()).commitTransaction(con); + return null; + }); + + } catch (StorageTransactionLogicException e) { + throw new StorageQueryException(e.actualException); + } + } } diff --git a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java index d14c38e6b..8596a28d8 100644 --- a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java +++ b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java @@ -17,26 +17,39 @@ package io.supertokens.authRecipe; import io.supertokens.Main; +import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; +import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException; +import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException; +import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithPrimaryUserIdException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; -import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; +import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage; +import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; import io.supertokens.pluginInterface.useridmapping.UserIdMapping; +import io.supertokens.session.Session; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.useridmapping.UserIdType; import org.jetbrains.annotations.TestOnly; import javax.annotation.Nullable; +import java.util.*; /*This files contains functions that are common for all auth recipes*/ @@ -44,6 +57,586 @@ public class AuthRecipe { public static final int USER_PAGINATION_LIMIT = 500; + @TestOnly + public static boolean unlinkAccounts(Main main, String recipeUserId) + throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException { + AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null, + StorageLayer.getStorage(main)); + return unlinkAccounts(main, appId, recipeUserId); + } + + + // returns true if the input user ID was deleted - which can happens if it was a primary user id and + // there were other accounts linked to it as well. + public static boolean unlinkAccounts(Main main, AppIdentifierWithStorage appIdentifierWithStorage, + String recipeUserId) + throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException { + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage(); + try { + UnlinkResult res = storage.startTransaction(con -> { + AuthRecipeUserInfo primaryUser = storage.getPrimaryUserById_Transaction(appIdentifierWithStorage, con, + recipeUserId); + if (primaryUser == null) { + throw new StorageTransactionLogicException(new UnknownUserIdException()); + } + + if (!primaryUser.isPrimaryUser) { + throw new StorageTransactionLogicException(new InputUserIdIsNotAPrimaryUserException(recipeUserId)); + } + + io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult = + io.supertokens.useridmapping.UserIdMapping.getUserIdMapping( + appIdentifierWithStorage, + recipeUserId, UserIdType.SUPERTOKENS); + + if (primaryUser.getSupertokensUserId().equals(recipeUserId)) { + // we are trying to unlink the user ID which is the same as the primary one. + if (primaryUser.loginMethods.length == 1) { + storage.unlinkAccounts_Transaction(appIdentifierWithStorage, con, primaryUser.getSupertokensUserId(), recipeUserId); + return new UnlinkResult(mappingResult == null ? recipeUserId : mappingResult.externalUserId, false); + } else { + // Here we delete the recipe user id cause if we just unlink, then there will be two + // distinct users with the same ID - which is a broken state. + // The delete will also cause the automatic unlinking. + // We need to make sure that it only deletes sessions for recipeUserId and not other linked + // users who have their sessions for primaryUserId (that is equal to the recipeUserId) + deleteUserHelper(con, appIdentifierWithStorage, recipeUserId, false, mappingResult); + return new UnlinkResult(mappingResult == null ? recipeUserId : mappingResult.externalUserId, true); + } + } else { + storage.unlinkAccounts_Transaction(appIdentifierWithStorage, con, primaryUser.getSupertokensUserId(), recipeUserId); + return new UnlinkResult(mappingResult == null ? recipeUserId : mappingResult.externalUserId, false); + } + }); + Session.revokeAllSessionsForUser(main, appIdentifierWithStorage, res.userId, false); + return res.wasLinked; + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof UnknownUserIdException) { + throw (UnknownUserIdException) e.actualException; + } else if (e.actualException instanceof InputUserIdIsNotAPrimaryUserException) { + throw (InputUserIdIsNotAPrimaryUserException) e.actualException; + } + throw new RuntimeException(e); + } + } + + @TestOnly + public static AuthRecipeUserInfo getUserById(Main main, String userId) + throws StorageQueryException { + AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null, + StorageLayer.getStorage(main)); + return getUserById(appId, userId); + } + + public static AuthRecipeUserInfo getUserById(AppIdentifierWithStorage appIdentifierWithStorage, String userId) + throws StorageQueryException { + return appIdentifierWithStorage.getAuthRecipeStorage().getPrimaryUserById(appIdentifierWithStorage, userId); + } + + public static class CreatePrimaryUserResult { + public AuthRecipeUserInfo user; + public boolean wasAlreadyAPrimaryUser; + + public CreatePrimaryUserResult(AuthRecipeUserInfo user, boolean wasAlreadyAPrimaryUser) { + this.user = user; + this.wasAlreadyAPrimaryUser = wasAlreadyAPrimaryUser; + } + } + + public static class CanLinkAccountsResult { + public String recipeUserId; + public String primaryUserId; + + public boolean alreadyLinked; + + public CanLinkAccountsResult(String recipeUserId, String primaryUserId, boolean alreadyLinked) { + this.recipeUserId = recipeUserId; + this.primaryUserId = primaryUserId; + this.alreadyLinked = alreadyLinked; + } + } + + @TestOnly + public static CanLinkAccountsResult canLinkAccounts(Main main, String recipeUserId, String primaryUserId) + throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException, + RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException, + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException { + AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null, + StorageLayer.getStorage(main)); + return canLinkAccounts(appId, recipeUserId, primaryUserId); + } + + public static CanLinkAccountsResult canLinkAccounts(AppIdentifierWithStorage appIdentifierWithStorage, + String recipeUserId, String primaryUserId) + throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException, + RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException, + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException { + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage(); + try { + return storage.startTransaction(con -> { + try { + CanLinkAccountsResult result = canLinkAccountsHelper(con, appIdentifierWithStorage, + recipeUserId, primaryUserId); + + storage.commitTransaction(con); + + return result; + } catch (UnknownUserIdException | InputUserIdIsNotAPrimaryUserException | + RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException | + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + throw new StorageTransactionLogicException(e); + } + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof UnknownUserIdException) { + throw (UnknownUserIdException) e.actualException; + } else if (e.actualException instanceof InputUserIdIsNotAPrimaryUserException) { + throw (InputUserIdIsNotAPrimaryUserException) e.actualException; + } else if (e.actualException instanceof RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException) { + throw (RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException) e.actualException; + } else if (e.actualException instanceof AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) { + throw (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) e.actualException; + } + throw new StorageQueryException(e); + } + } + + private static CanLinkAccountsResult canLinkAccountsHelper(TransactionConnection con, + AppIdentifierWithStorage appIdentifierWithStorage, + String _recipeUserId, String _primaryUserId) + throws StorageQueryException, UnknownUserIdException, InputUserIdIsNotAPrimaryUserException, + RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException, + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException { + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage(); + AuthRecipeUserInfo primaryUser = storage.getPrimaryUserById_Transaction(appIdentifierWithStorage, con, + _primaryUserId); + + if (primaryUser == null) { + throw new UnknownUserIdException(); + } + + if (!primaryUser.isPrimaryUser) { + throw new InputUserIdIsNotAPrimaryUserException(primaryUser.getSupertokensUserId()); + } + + AuthRecipeUserInfo recipeUser = storage.getPrimaryUserById_Transaction(appIdentifierWithStorage, con, + _recipeUserId); + if (recipeUser == null) { + throw new UnknownUserIdException(); + } + + if (recipeUser.isPrimaryUser) { + if (recipeUser.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) { + return new CanLinkAccountsResult(recipeUser.getSupertokensUserId(), primaryUser.getSupertokensUserId(), true); + } else { + throw new RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(recipeUser, + "The input recipe user ID is already linked to another user ID"); + } + } + + // now we know that the recipe user ID is not a primary user, so we can focus on it's one + // login method + assert (recipeUser.loginMethods.length == 1); + LoginMethod recipeUserIdLM = recipeUser.loginMethods[0]; + + Set tenantIds = new HashSet<>(); + tenantIds.addAll(recipeUser.tenantIds); + tenantIds.addAll(primaryUser.tenantIds); + + // we loop through the union of both the user's tenantIds and check that the criteria for + // linking accounts is not violated in any of them. We do a union and not an intersection + // cause if we did an intersection, and that yields that account linking is allowed, it could + // result in one tenant having two primary users with the same email. For example: + // - tenant1 has u1 with email e, and u2 with email e, primary user (one is ep, one is tp) + // - tenant2 has u3 with email e, primary user (passwordless) + // now if we want to link u3 with u1, we have to deny it cause if we don't, it will result in + // u1 and u2 to be primary users with the same email in the same tenant. If we do an + // intersection, we will get an empty set, but if we do a union, we will get both the tenants and + // do the checks in both. + for (String tenantId : tenantIds) { + TenantIdentifier tenantIdentifier = new TenantIdentifier( + appIdentifierWithStorage.getConnectionUriDomain(), appIdentifierWithStorage.getAppId(), + tenantId); + // we do not bother with getting the tenantIdentifierWithStorage here because + // we get the tenants from the user itself, and the user can only be shared across + // tenants of the same storage - therefore, the storage will be the same. + + if (recipeUserIdLM.email != null) { + AuthRecipeUserInfo[] usersWithSameEmail = storage + .listPrimaryUsersByEmail_Transaction(appIdentifierWithStorage, con, + recipeUserIdLM.email); + for (AuthRecipeUserInfo user : usersWithSameEmail) { + if (!user.tenantIds.contains(tenantId)) { + continue; + } + if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) { + throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(), + "This user's email is already associated with another user ID"); + } + } + } + + if (recipeUserIdLM.phoneNumber != null) { + AuthRecipeUserInfo[] usersWithSamePhoneNumber = storage + .listPrimaryUsersByPhoneNumber_Transaction(appIdentifierWithStorage, con, + recipeUserIdLM.phoneNumber); + for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) { + if (!user.tenantIds.contains(tenantId)) { + continue; + } + if (user.isPrimaryUser && !user.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) { + throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(), + "This user's phone number is already associated with another user" + + " ID"); + } + } + } + + if (recipeUserIdLM.thirdParty != null) { + AuthRecipeUserInfo[] usersWithSameThirdParty = storage + .listPrimaryUsersByThirdPartyInfo_Transaction(appIdentifierWithStorage, con, + recipeUserIdLM.thirdParty.id, recipeUserIdLM.thirdParty.userId); + for (AuthRecipeUserInfo userWithSameThirdParty : usersWithSameThirdParty) { + if (!userWithSameThirdParty.tenantIds.contains(tenantId)) { + continue; + } + if (userWithSameThirdParty.isPrimaryUser && + !userWithSameThirdParty.getSupertokensUserId().equals(primaryUser.getSupertokensUserId())) { + throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( + userWithSameThirdParty.getSupertokensUserId(), + "This user's third party login is already associated with another" + + " user ID"); + } + } + + } + } + + return new CanLinkAccountsResult(recipeUser.getSupertokensUserId(), primaryUser.getSupertokensUserId(), false); + } + + @TestOnly + public static LinkAccountsResult linkAccounts(Main main, String recipeUserId, String primaryUserId) + throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException, + UnknownUserIdException, + FeatureNotEnabledException, InputUserIdIsNotAPrimaryUserException, + RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException { + AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null, + StorageLayer.getStorage(main)); + try { + return linkAccounts(main, appId, recipeUserId, primaryUserId); + } catch (TenantOrAppNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static LinkAccountsResult linkAccounts(Main main, AppIdentifierWithStorage appIdentifierWithStorage, + String _recipeUserId, String _primaryUserId) + throws StorageQueryException, + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException, + RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException, InputUserIdIsNotAPrimaryUserException, + UnknownUserIdException, TenantOrAppNotFoundException, FeatureNotEnabledException { + + if (Arrays.stream(FeatureFlag.getInstance(main, appIdentifierWithStorage).getEnabledFeatures()) + .noneMatch(t -> t == EE_FEATURES.ACCOUNT_LINKING)) { + throw new FeatureNotEnabledException( + "Account linking feature is not enabled for this app. Please contact support to enable it."); + } + + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage(); + try { + LinkAccountsResult result = storage.startTransaction(con -> { + + try { + CanLinkAccountsResult canLinkAccounts = canLinkAccountsHelper(con, appIdentifierWithStorage, + _recipeUserId, _primaryUserId); + + if (canLinkAccounts.alreadyLinked) { + return new LinkAccountsResult(getUserById(appIdentifierWithStorage, canLinkAccounts.primaryUserId), true); + } + // now we can link accounts in the db. + storage.linkAccounts_Transaction(appIdentifierWithStorage, con, canLinkAccounts.recipeUserId, + canLinkAccounts.primaryUserId); + + storage.commitTransaction(con); + + return new LinkAccountsResult(getUserById(appIdentifierWithStorage, canLinkAccounts.primaryUserId), false); + } catch (UnknownUserIdException | InputUserIdIsNotAPrimaryUserException | + RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException | + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + throw new StorageTransactionLogicException(e); + } + }); + + if (!result.wasAlreadyLinked) { + io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult = + io.supertokens.useridmapping.UserIdMapping.getUserIdMapping( + appIdentifierWithStorage, + _recipeUserId, UserIdType.SUPERTOKENS); + // finally, we revoke all sessions of the recipeUser Id cause their user ID has changed. + Session.revokeAllSessionsForUser(main, appIdentifierWithStorage, + mappingResult == null ? _recipeUserId : mappingResult.externalUserId, false); + } + + return result; + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof UnknownUserIdException) { + throw (UnknownUserIdException) e.actualException; + } else if (e.actualException instanceof InputUserIdIsNotAPrimaryUserException) { + throw (InputUserIdIsNotAPrimaryUserException) e.actualException; + } else if (e.actualException instanceof RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException) { + throw (RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException) e.actualException; + } else if (e.actualException instanceof AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) { + throw (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) e.actualException; + } + throw new StorageQueryException(e); + } + } + + public static class LinkAccountsResult { + public final AuthRecipeUserInfo user; + public final boolean wasAlreadyLinked; + + public LinkAccountsResult(AuthRecipeUserInfo user, boolean wasAlreadyLinked) { + this.user = user; + this.wasAlreadyLinked = wasAlreadyLinked; + } + } + + @TestOnly + public static CreatePrimaryUserResult canCreatePrimaryUser(Main main, + String recipeUserId) + throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException, + RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException { + AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null, + StorageLayer.getStorage(main)); + return canCreatePrimaryUser(appId, recipeUserId); + } + + public static CreatePrimaryUserResult canCreatePrimaryUser(AppIdentifierWithStorage appIdentifierWithStorage, + String recipeUserId) + throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException, + RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException { + + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage(); + try { + return storage.startTransaction(con -> { + try { + return canCreatePrimaryUserHelper(con, appIdentifierWithStorage, + recipeUserId); + + } catch (UnknownUserIdException | RecipeUserIdAlreadyLinkedWithPrimaryUserIdException | + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + throw new StorageTransactionLogicException(e); + } + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof UnknownUserIdException) { + throw (UnknownUserIdException) e.actualException; + } else if (e.actualException instanceof RecipeUserIdAlreadyLinkedWithPrimaryUserIdException) { + throw (RecipeUserIdAlreadyLinkedWithPrimaryUserIdException) e.actualException; + } else if (e.actualException instanceof AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) { + throw (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) e.actualException; + } + throw new StorageQueryException(e); + } + } + + private static CreatePrimaryUserResult canCreatePrimaryUserHelper(TransactionConnection con, + AppIdentifierWithStorage appIdentifierWithStorage, + String recipeUserId) + throws StorageQueryException, UnknownUserIdException, RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException { + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage(); + + AuthRecipeUserInfo targetUser = storage.getPrimaryUserById_Transaction(appIdentifierWithStorage, con, + recipeUserId); + if (targetUser == null) { + throw new UnknownUserIdException(); + } + if (targetUser.isPrimaryUser) { + if (targetUser.getSupertokensUserId().equals(recipeUserId)) { + return new CreatePrimaryUserResult(targetUser, true); + } else { + throw new RecipeUserIdAlreadyLinkedWithPrimaryUserIdException(targetUser.getSupertokensUserId(), + "This user ID is already linked to another user ID"); + } + } + + // this means that the user has only one login method since it's not a primary user + // nor is it linked to a primary user + assert (targetUser.loginMethods.length == 1); + LoginMethod loginMethod = targetUser.loginMethods[0]; + + for (String tenantId : targetUser.tenantIds) { + if (loginMethod.email != null) { + AuthRecipeUserInfo[] usersWithSameEmail = storage + .listPrimaryUsersByEmail_Transaction(appIdentifierWithStorage, con, + loginMethod.email); + for (AuthRecipeUserInfo user : usersWithSameEmail) { + if (!user.tenantIds.contains(tenantId)) { + continue; + } + if (user.isPrimaryUser) { + throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(), + "This user's email is already associated with another user ID"); + } + } + } + + if (loginMethod.phoneNumber != null) { + AuthRecipeUserInfo[] usersWithSamePhoneNumber = storage + .listPrimaryUsersByPhoneNumber_Transaction(appIdentifierWithStorage, con, + loginMethod.phoneNumber); + for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) { + if (!user.tenantIds.contains(tenantId)) { + continue; + } + if (user.isPrimaryUser) { + throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(), + "This user's phone number is already associated with another user" + + " ID"); + } + } + } + + if (loginMethod.thirdParty != null) { + AuthRecipeUserInfo[] usersWithSameThirdParty = storage + .listPrimaryUsersByThirdPartyInfo_Transaction(appIdentifierWithStorage, con, + loginMethod.thirdParty.id, loginMethod.thirdParty.userId); + for (AuthRecipeUserInfo userWithSameThirdParty : usersWithSameThirdParty) { + if (!userWithSameThirdParty.tenantIds.contains(tenantId)) { + continue; + } + if (userWithSameThirdParty.isPrimaryUser) { + throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException( + userWithSameThirdParty.getSupertokensUserId(), + "This user's third party login is already associated with another" + + " user ID"); + } + } + } + } + + return new CreatePrimaryUserResult(targetUser, false); + } + + @TestOnly + public static CreatePrimaryUserResult createPrimaryUser(Main main, + String recipeUserId) + throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException, + RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException, + FeatureNotEnabledException { + AppIdentifierWithStorage appId = new AppIdentifierWithStorage(null, null, + StorageLayer.getStorage(main)); + try { + return createPrimaryUser(main, appId, recipeUserId); + } catch (TenantOrAppNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static CreatePrimaryUserResult createPrimaryUser(Main main, + AppIdentifierWithStorage appIdentifierWithStorage, + String recipeUserId) + throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException, + RecipeUserIdAlreadyLinkedWithPrimaryUserIdException, UnknownUserIdException, TenantOrAppNotFoundException, + FeatureNotEnabledException { + + if (Arrays.stream(FeatureFlag.getInstance(main, appIdentifierWithStorage).getEnabledFeatures()) + .noneMatch(t -> t == EE_FEATURES.ACCOUNT_LINKING)) { + throw new FeatureNotEnabledException( + "Account linking feature is not enabled for this app. Please contact support to enable it."); + } + + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage(); + try { + return storage.startTransaction(con -> { + + try { + CreatePrimaryUserResult result = canCreatePrimaryUserHelper(con, appIdentifierWithStorage, + recipeUserId); + if (result.wasAlreadyAPrimaryUser) { + return result; + } + storage.makePrimaryUser_Transaction(appIdentifierWithStorage, con, result.user.getSupertokensUserId()); + + storage.commitTransaction(con); + + result.user.isPrimaryUser = true; + + return result; + } catch (UnknownUserIdException | RecipeUserIdAlreadyLinkedWithPrimaryUserIdException | + AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + throw new StorageTransactionLogicException(e); + } + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof UnknownUserIdException) { + throw (UnknownUserIdException) e.actualException; + } else if (e.actualException instanceof RecipeUserIdAlreadyLinkedWithPrimaryUserIdException) { + throw (RecipeUserIdAlreadyLinkedWithPrimaryUserIdException) e.actualException; + } else if (e.actualException instanceof AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) { + throw (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException) e.actualException; + } + throw new StorageQueryException(e); + } + } + + public static AuthRecipeUserInfo[] getUsersByAccountInfo(TenantIdentifierWithStorage tenantIdentifier, + boolean doUnionOfAccountInfo, String email, + String phoneNumber, String thirdPartyId, + String thirdPartyUserId) + throws StorageQueryException { + Set result = new HashSet<>(); + + if (email != null) { + AuthRecipeUserInfo[] users = tenantIdentifier.getAuthRecipeStorage() + .listPrimaryUsersByEmail(tenantIdentifier, email); + result.addAll(List.of(users)); + } + if (phoneNumber != null) { + AuthRecipeUserInfo[] users = tenantIdentifier.getAuthRecipeStorage() + .listPrimaryUsersByPhoneNumber(tenantIdentifier, phoneNumber); + result.addAll(List.of(users)); + } + if (thirdPartyId != null && thirdPartyUserId != null) { + AuthRecipeUserInfo user = tenantIdentifier.getAuthRecipeStorage() + .getPrimaryUserByThirdPartyInfo(tenantIdentifier, thirdPartyId, thirdPartyUserId); + if (user != null) { + result.add(user); + } + } + + if (doUnionOfAccountInfo) { + return result.toArray(new AuthRecipeUserInfo[0]); + } else { + List finalList = new ArrayList<>(); + for (AuthRecipeUserInfo user : result) { + boolean emailMatch = email == null; + boolean phoneNumberMatch = phoneNumber == null; + boolean thirdPartyMatch = thirdPartyId == null; + for (LoginMethod lM : user.loginMethods) { + if (email != null && email.equals(lM.email)) { + emailMatch = true; + } + if (phoneNumber != null && phoneNumber.equals(lM.phoneNumber)) { + phoneNumberMatch = true; + } + if (thirdPartyId != null && + (new LoginMethod.ThirdParty(thirdPartyId, thirdPartyUserId)).equals(lM.thirdParty)) { + thirdPartyMatch = true; + } + } + if (emailMatch && phoneNumberMatch && thirdPartyMatch) { + finalList.add(user); + } + } + return finalList.toArray(new AuthRecipeUserInfo[0]); + } + + } + public static long getUsersCountForTenant(TenantIdentifierWithStorage tenantIdentifier, RECIPE_ID[] includeRecipeIds) throws StorageQueryException, @@ -110,7 +703,7 @@ public static UserPaginationContainer getUsers(TenantIdentifierWithStorage tenan int maxLoop = users.length; if (users.length == limit + 1) { maxLoop = limit; - nextPaginationToken = new UserPaginationToken(users[limit].id, + nextPaginationToken = new UserPaginationToken(users[limit].getSupertokensUserId(), users[limit].timeJoined).generateToken(); } AuthRecipeUserInfo[] resultUsers = new AuthRecipeUserInfo[maxLoop]; @@ -135,9 +728,36 @@ public static UserPaginationContainer getUsers(Main main, } } + @TestOnly public static void deleteUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId, UserIdMapping userIdMapping) throws StorageQueryException, StorageTransactionLogicException { + deleteUser(appIdentifierWithStorage, userId, true, userIdMapping); + } + + public static void deleteUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId, + boolean removeAllLinkedAccounts, + UserIdMapping userIdMapping) + throws StorageQueryException, StorageTransactionLogicException { + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage(); + + storage.startTransaction(con -> { + deleteUserHelper(con, appIdentifierWithStorage, userId, removeAllLinkedAccounts, userIdMapping); + storage.commitTransaction(con); + return null; + }); + } + + private static void deleteUserHelper(TransactionConnection con, AppIdentifierWithStorage appIdentifierWithStorage, + String userId, + boolean removeAllLinkedAccounts, + UserIdMapping userIdMapping) + throws StorageQueryException { + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage(); + + String userIdToDeleteForNonAuthRecipeForRecipeUserId; + String userIdToDeleteForAuthRecipe; + // We clean up the user last so that if anything before that throws an error, then that will throw a // 500 to the // developer. In this case, they expect that the user has not been deleted (which will be true). This @@ -163,24 +783,106 @@ public static void deleteUser(AppIdentifierWithStorage appIdentifierWithStorage, // in reference to // https://docs.google.com/spreadsheets/d/17hYV32B0aDCeLnSxbZhfRN2Y9b0LC2xUF44vV88RNAA/edit?usp=sharing // we want to check which state the db is in - if (appIdentifierWithStorage.getAuthRecipeStorage() - .doesUserIdExist(appIdentifierWithStorage, userIdMapping.externalUserId)) { + if (((AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage()) + .doesUserIdExist_Transaction(con, appIdentifierWithStorage, userIdMapping.externalUserId)) { // db is in state A4 // delete only from auth tables - deleteAuthRecipeUser(appIdentifierWithStorage, userId); + userIdToDeleteForAuthRecipe = userId; + userIdToDeleteForNonAuthRecipeForRecipeUserId = null; } else { // db is in state A3 // delete user from non-auth tables with externalUserId - deleteNonAuthRecipeUser(appIdentifierWithStorage, userIdMapping.externalUserId); - // delete user from auth tables with superTokensUserId - deleteAuthRecipeUser(appIdentifierWithStorage, userIdMapping.superTokensUserId); + userIdToDeleteForAuthRecipe = userIdMapping.superTokensUserId; + userIdToDeleteForNonAuthRecipeForRecipeUserId = userIdMapping.externalUserId; } } else { - deleteNonAuthRecipeUser(appIdentifierWithStorage, userId); - deleteAuthRecipeUser(appIdentifierWithStorage, userId); + userIdToDeleteForAuthRecipe = userId; + userIdToDeleteForNonAuthRecipeForRecipeUserId = userId; + } + + assert (userIdToDeleteForAuthRecipe != null); + + // this user ID represents the non auth recipe stuff to delete for the primary user id + String primaryUserIdToDeleteNonAuthRecipe = null; + + AuthRecipeUserInfo userToDelete = storage.getPrimaryUserById_Transaction(appIdentifierWithStorage, con, + userIdToDeleteForAuthRecipe); + + if (userToDelete == null) { + return; + } + + if (removeAllLinkedAccounts || userToDelete.loginMethods.length == 1) { + if (userToDelete.getSupertokensUserId().equals(userIdToDeleteForAuthRecipe)) { + primaryUserIdToDeleteNonAuthRecipe = userIdToDeleteForNonAuthRecipeForRecipeUserId; + if (primaryUserIdToDeleteNonAuthRecipe == null) { + deleteAuthRecipeUser(con, appIdentifierWithStorage, userToDelete.getSupertokensUserId(), + true); + return; + } + } else { + // this is always type supertokens user ID cause it's from a user from the database. + io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult = + io.supertokens.useridmapping.UserIdMapping.getUserIdMapping( + con, + appIdentifierWithStorage, + userToDelete.getSupertokensUserId(), UserIdType.SUPERTOKENS); + if (mappingResult != null) { + primaryUserIdToDeleteNonAuthRecipe = mappingResult.externalUserId; + } else { + primaryUserIdToDeleteNonAuthRecipe = userToDelete.getSupertokensUserId(); + } + + } + } else { + if (userToDelete.getSupertokensUserId().equals(userIdToDeleteForAuthRecipe)) { + // this means we are deleting the primary user itself, but keeping other linked accounts + // so we keep the non auth recipe info of this user since other linked accounts can use it + userIdToDeleteForNonAuthRecipeForRecipeUserId = null; + } + } + + if (!removeAllLinkedAccounts) { + deleteAuthRecipeUser(con, appIdentifierWithStorage, userIdToDeleteForAuthRecipe, + !userIdToDeleteForAuthRecipe.equals(userToDelete.getSupertokensUserId())); + + if (userIdToDeleteForNonAuthRecipeForRecipeUserId != null) { + deleteNonAuthRecipeUser(con, appIdentifierWithStorage, userIdToDeleteForNonAuthRecipeForRecipeUserId); + } + + if (primaryUserIdToDeleteNonAuthRecipe != null) { + deleteNonAuthRecipeUser(con, appIdentifierWithStorage, primaryUserIdToDeleteNonAuthRecipe); + + // this is only done to also delete the user ID mapping in case it exists, since we do not delete in the + // previous call to deleteAuthRecipeUser above. + deleteAuthRecipeUser(con, appIdentifierWithStorage, userToDelete.getSupertokensUserId(), + true); + } + } else { + for (LoginMethod lM : userToDelete.loginMethods) { + io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult = lM.getSupertokensUserId().equals( + userIdToDeleteForAuthRecipe) ? userIdMapping : + io.supertokens.useridmapping.UserIdMapping.getUserIdMapping( + con, + appIdentifierWithStorage, + lM.getSupertokensUserId(), UserIdType.SUPERTOKENS); + deleteUserHelper(con, appIdentifierWithStorage, lM.getSupertokensUserId(), false, mappingResult); + } } } + @TestOnly + public static void deleteUser(Main main, String userId, boolean removeAllLinkedAccounts) + throws StorageQueryException, StorageTransactionLogicException { + Storage storage = StorageLayer.getStorage(main); + AppIdentifierWithStorage appIdentifier = new AppIdentifierWithStorage( + null, null, storage); + UserIdMapping mapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(appIdentifier, + userId, UserIdType.ANY); + + deleteUser(appIdentifier, userId, removeAllLinkedAccounts, mapping); + } + @TestOnly public static void deleteUser(Main main, String userId) throws StorageQueryException, StorageTransactionLogicException { @@ -203,30 +905,37 @@ public static void deleteUser(AppIdentifierWithStorage appIdentifierWithStorage, deleteUser(appIdentifierWithStorage, userId, mapping); } - private static void deleteNonAuthRecipeUser(AppIdentifierWithStorage - appIdentifierWithStorage, String userId) - throws StorageQueryException, StorageTransactionLogicException { + private static void deleteNonAuthRecipeUser(TransactionConnection con, AppIdentifierWithStorage + appIdentifierWithStorage, String userId) + throws StorageQueryException { appIdentifierWithStorage.getUserMetadataStorage() - .deleteUserMetadata(appIdentifierWithStorage, userId); - appIdentifierWithStorage.getSessionStorage() - .deleteSessionsOfUser(appIdentifierWithStorage, userId); + .deleteUserMetadata_Transaction(con, appIdentifierWithStorage, userId); + ((SessionSQLStorage) appIdentifierWithStorage.getSessionStorage()) + .deleteSessionsOfUser_Transaction(con, appIdentifierWithStorage, userId); appIdentifierWithStorage.getEmailVerificationStorage() - .deleteEmailVerificationUserInfo(appIdentifierWithStorage, userId); + .deleteEmailVerificationUserInfo_Transaction(con, appIdentifierWithStorage, userId); appIdentifierWithStorage.getUserRolesStorage() - .deleteAllRolesForUser(appIdentifierWithStorage, userId); + .deleteAllRolesForUser_Transaction(con, appIdentifierWithStorage, userId); appIdentifierWithStorage.getActiveUsersStorage() - .deleteUserActive(appIdentifierWithStorage, userId); + .deleteUserActive_Transaction(con, appIdentifierWithStorage, userId); + appIdentifierWithStorage.getTOTPStorage().removeUser_Transaction(con, appIdentifierWithStorage, userId); + } - TOTPSQLStorage storage = appIdentifierWithStorage.getTOTPStorage(); - storage.startTransaction(con -> { - storage.removeUser_Transaction(con, appIdentifierWithStorage, userId); - storage.commitTransaction(con); - return null; - }); + private static void deleteAuthRecipeUser(TransactionConnection con, + AppIdentifierWithStorage appIdentifierWithStorage, String + userId, boolean deleteFromUserIdToAppIdTableToo) + throws StorageQueryException { + // auth recipe deletions here only + appIdentifierWithStorage.getEmailPasswordStorage() + .deleteEmailPasswordUser_Transaction(con, appIdentifierWithStorage, userId, deleteFromUserIdToAppIdTableToo); + appIdentifierWithStorage.getThirdPartyStorage() + .deleteThirdPartyUser_Transaction(con, appIdentifierWithStorage, userId, deleteFromUserIdToAppIdTableToo); + appIdentifierWithStorage.getPasswordlessStorage() + .deletePasswordlessUser_Transaction(con, appIdentifierWithStorage, userId, deleteFromUserIdToAppIdTableToo); } public static boolean deleteNonAuthRecipeUser(TenantIdentifierWithStorage - tenantIdentifierWithStorage, String userId) + tenantIdentifierWithStorage, String userId) throws StorageQueryException { // UserMetadata is per app, so nothing to delete @@ -253,12 +962,13 @@ public static boolean deleteNonAuthRecipeUser(TenantIdentifierWithStorage return finalDidExist; } - private static void deleteAuthRecipeUser(AppIdentifierWithStorage appIdentifierWithStorage, String - userId) - throws StorageQueryException { - // auth recipe deletions here only - appIdentifierWithStorage.getEmailPasswordStorage().deleteEmailPasswordUser(appIdentifierWithStorage, userId); - appIdentifierWithStorage.getThirdPartyStorage().deleteThirdPartyUser(appIdentifierWithStorage, userId); - appIdentifierWithStorage.getPasswordlessStorage().deletePasswordlessUser(appIdentifierWithStorage, userId); + private static class UnlinkResult { + public final String userId; + public final boolean wasLinked; + + public UnlinkResult(String userId, boolean wasLinked) { + this.userId = userId; + this.wasLinked = wasLinked; + } } } diff --git a/src/main/java/io/supertokens/authRecipe/UserPaginationContainer.java b/src/main/java/io/supertokens/authRecipe/UserPaginationContainer.java index f1eb37560..d4ef616aa 100644 --- a/src/main/java/io/supertokens/authRecipe/UserPaginationContainer.java +++ b/src/main/java/io/supertokens/authRecipe/UserPaginationContainer.java @@ -22,24 +22,11 @@ import javax.annotation.Nullable; public class UserPaginationContainer { - public final UsersContainer[] users; + public final AuthRecipeUserInfo[] users; public final String nextPaginationToken; public UserPaginationContainer(@Nonnull AuthRecipeUserInfo[] users, @Nullable String nextPaginationToken) { - this.users = new UsersContainer[users.length]; - for (int i = 0; i < users.length; i++) { - this.users[i] = new UsersContainer(users[i]); - } + this.users = users; this.nextPaginationToken = nextPaginationToken; } - - public static class UsersContainer { - public final AuthRecipeUserInfo user; - public final String recipeId; - - public UsersContainer(AuthRecipeUserInfo user) { - this.user = user; - this.recipeId = user.getRecipeId().toString(); - } - } } diff --git a/src/main/java/io/supertokens/emailpassword/UserPaginationContainer.java b/src/main/java/io/supertokens/authRecipe/exception/AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException.java similarity index 55% rename from src/main/java/io/supertokens/emailpassword/UserPaginationContainer.java rename to src/main/java/io/supertokens/authRecipe/exception/AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException.java index fa112e2dc..7a2805f1e 100644 --- a/src/main/java/io/supertokens/emailpassword/UserPaginationContainer.java +++ b/src/main/java/io/supertokens/authRecipe/exception/AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. * * This software is licensed under the Apache License, Version 2.0 (the * "License") as published by the Apache Software Foundation. @@ -14,19 +14,13 @@ * under the License. */ -package io.supertokens.emailpassword; +package io.supertokens.authRecipe.exception; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +public class AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException extends Exception { + public final String primaryUserId; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public class UserPaginationContainer { - public final UserInfo[] users; - public final String nextPaginationToken; - - public UserPaginationContainer(@Nonnull UserInfo[] users, @Nullable String nextPaginationToken) { - this.users = users; - this.nextPaginationToken = nextPaginationToken; + public AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(String primaryUserId, String description) { + super(description); + this.primaryUserId = primaryUserId; } } diff --git a/src/main/java/io/supertokens/authRecipe/exception/InputUserIdIsNotAPrimaryUserException.java b/src/main/java/io/supertokens/authRecipe/exception/InputUserIdIsNotAPrimaryUserException.java new file mode 100644 index 000000000..b0fdc333a --- /dev/null +++ b/src/main/java/io/supertokens/authRecipe/exception/InputUserIdIsNotAPrimaryUserException.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.authRecipe.exception; + +public class InputUserIdIsNotAPrimaryUserException extends Exception { + public final String userId; + + public InputUserIdIsNotAPrimaryUserException(String userId) { + super(); + this.userId = userId; + } +} diff --git a/src/main/java/io/supertokens/thirdparty/UserPaginationContainer.java b/src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException.java similarity index 56% rename from src/main/java/io/supertokens/thirdparty/UserPaginationContainer.java rename to src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException.java index cf361ea96..737929a62 100644 --- a/src/main/java/io/supertokens/thirdparty/UserPaginationContainer.java +++ b/src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. * * This software is licensed under the Apache License, Version 2.0 (the * "License") as published by the Apache Software Foundation. @@ -14,19 +14,15 @@ * under the License. */ -package io.supertokens.thirdparty; +package io.supertokens.authRecipe.exception; -import io.supertokens.pluginInterface.thirdparty.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +public class RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException extends Exception { + public final AuthRecipeUserInfo recipeUser; -public class UserPaginationContainer { - public final UserInfo[] users; - public final String nextPaginationToken; - - public UserPaginationContainer(@Nonnull UserInfo[] users, @Nullable String nextPaginationToken) { - this.users = users; - this.nextPaginationToken = nextPaginationToken; + public RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(AuthRecipeUserInfo recipeUser, String description) { + super(description); + this.recipeUser = recipeUser; } } diff --git a/src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithPrimaryUserIdException.java b/src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithPrimaryUserIdException.java new file mode 100644 index 000000000..7d0bfe990 --- /dev/null +++ b/src/main/java/io/supertokens/authRecipe/exception/RecipeUserIdAlreadyLinkedWithPrimaryUserIdException.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.authRecipe.exception; + +public class RecipeUserIdAlreadyLinkedWithPrimaryUserIdException extends Exception { + public final String primaryUserId; + + public RecipeUserIdAlreadyLinkedWithPrimaryUserIdException(String primaryUserId, String description) { + super(description); + this.primaryUserId = primaryUserId; + } +} diff --git a/src/main/java/io/supertokens/emailpassword/EmailPassword.java b/src/main/java/io/supertokens/emailpassword/EmailPassword.java index 362772188..0c7fb08a0 100644 --- a/src/main/java/io/supertokens/emailpassword/EmailPassword.java +++ b/src/main/java/io/supertokens/emailpassword/EmailPassword.java @@ -17,16 +17,21 @@ package io.supertokens.emailpassword; import io.supertokens.Main; +import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.config.Config; import io.supertokens.config.CoreConfig; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.emailpassword.exceptions.ResetPasswordInvalidTokenException; import io.supertokens.emailpassword.exceptions.UnsupportedPasswordHashingFormatException; import io.supertokens.emailpassword.exceptions.WrongCredentialsException; import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo; -import io.supertokens.pluginInterface.emailpassword.UserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicatePasswordResetTokenException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException; @@ -34,10 +39,14 @@ import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; -import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; +import io.supertokens.pluginInterface.multitenancy.TenantConfig; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.utils.Utils; +import io.supertokens.webserver.WebserverAPI; import org.jetbrains.annotations.TestOnly; import javax.annotation.Nonnull; @@ -50,9 +59,9 @@ public class EmailPassword { public static class ImportUserResponse { public boolean didUserAlreadyExist; - public UserInfo user; + public AuthRecipeUserInfo user; - public ImportUserResponse(boolean didUserAlreadyExist, UserInfo user) { + public ImportUserResponse(boolean didUserAlreadyExist, AuthRecipeUserInfo user) { this.didUserAlreadyExist = didUserAlreadyExist; this.user = user; } @@ -73,7 +82,7 @@ private static long getPasswordResetTokenLifetime(TenantIdentifier tenantIdentif } @TestOnly - public static UserInfo signUp(Main main, @Nonnull String email, @Nonnull String password) + public static AuthRecipeUserInfo signUp(Main main, @Nonnull String email, @Nonnull String password) throws DuplicateEmailException, StorageQueryException { try { Storage storage = StorageLayer.getStorage(main); @@ -84,7 +93,7 @@ public static UserInfo signUp(Main main, @Nonnull String email, @Nonnull String } } - public static UserInfo signUp(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, + public static AuthRecipeUserInfo signUp(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, @Nonnull String email, @Nonnull String password) throws DuplicateEmailException, StorageQueryException, TenantOrAppNotFoundException, BadPermissionException { @@ -106,7 +115,8 @@ public static UserInfo signUp(TenantIdentifierWithStorage tenantIdentifierWithSt long timeJoined = System.currentTimeMillis(); try { - return tenantIdentifierWithStorage.getEmailPasswordStorage().signUp(tenantIdentifierWithStorage, userId, email, hashedPassword, timeJoined); + return tenantIdentifierWithStorage.getEmailPasswordStorage() + .signUp(tenantIdentifierWithStorage, userId, email, hashedPassword, timeJoined); } catch (DuplicateUserIdException ignored) { // we retry with a new userId (while loop) @@ -117,7 +127,7 @@ public static UserInfo signUp(TenantIdentifierWithStorage tenantIdentifierWithSt @TestOnly public static ImportUserResponse importUserWithPasswordHash(Main main, @Nonnull String email, @Nonnull String passwordHash, @Nullable - CoreConfig.PASSWORD_HASHING_ALG hashingAlgorithm) + CoreConfig.PASSWORD_HASHING_ALG hashingAlgorithm) throws StorageQueryException, StorageTransactionLogicException, UnsupportedPasswordHashingFormatException { try { Storage storage = StorageLayer.getStorage(main); @@ -133,7 +143,7 @@ public static ImportUserResponse importUserWithPasswordHash(Main main, @Nonnull public static ImportUserResponse importUserWithPasswordHash(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, @Nonnull String email, @Nonnull String passwordHash, @Nullable - CoreConfig.PASSWORD_HASHING_ALG hashingAlgorithm) + CoreConfig.PASSWORD_HASHING_ALG hashingAlgorithm) throws StorageQueryException, StorageTransactionLogicException, UnsupportedPasswordHashingFormatException, TenantOrAppNotFoundException, BadPermissionException { @@ -145,7 +155,8 @@ public static ImportUserResponse importUserWithPasswordHash(TenantIdentifierWith throw new BadPermissionException("Email password login not enabled for tenant"); } - PasswordHashingUtils.assertSuperTokensSupportInputPasswordHashFormat(tenantIdentifierWithStorage.toAppIdentifier(), main, + PasswordHashingUtils.assertSuperTokensSupportInputPasswordHashFormat( + tenantIdentifierWithStorage.toAppIdentifier(), main, passwordHash, hashingAlgorithm); while (true) { @@ -155,18 +166,30 @@ public static ImportUserResponse importUserWithPasswordHash(TenantIdentifierWith EmailPasswordSQLStorage storage = tenantIdentifierWithStorage.getEmailPasswordStorage(); try { - UserInfo userInfo = storage.signUp(tenantIdentifierWithStorage, userId, email, passwordHash, + AuthRecipeUserInfo userInfo = storage.signUp(tenantIdentifierWithStorage, userId, email, passwordHash, timeJoined); return new ImportUserResponse(false, userInfo); } catch (DuplicateUserIdException e) { // we retry with a new userId } catch (DuplicateEmailException e) { - UserInfo userInfoToBeUpdated = storage.getUserInfoUsingEmail(tenantIdentifierWithStorage, email); + AuthRecipeUserInfo[] allUsers = storage.listPrimaryUsersByEmail(tenantIdentifierWithStorage, email); + AuthRecipeUserInfo userInfoToBeUpdated = null; + LoginMethod loginMethod = null; + for (AuthRecipeUserInfo currUser : allUsers) { + for (LoginMethod currLM : currUser.loginMethods) { + if (currLM.email.equals(email) && currLM.recipeId == RECIPE_ID.EMAIL_PASSWORD && currLM.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { + userInfoToBeUpdated = currUser; + loginMethod = currLM; + break; + } + } + } if (userInfoToBeUpdated != null) { + LoginMethod finalLoginMethod = loginMethod; storage.startTransaction(con -> { storage.updateUsersPassword_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, - userInfoToBeUpdated.id, passwordHash); + finalLoginMethod.getSupertokensUserId(), passwordHash); return null; }); return new ImportUserResponse(true, userInfoToBeUpdated); @@ -190,8 +213,8 @@ public static ImportUserResponse importUserWithPasswordHash(Main main, @Nonnull } @TestOnly - public static UserInfo signIn(Main main, @Nonnull String email, - @Nonnull String password) + public static AuthRecipeUserInfo signIn(Main main, @Nonnull String email, + @Nonnull String password) throws StorageQueryException, WrongCredentialsException { try { Storage storage = StorageLayer.getStorage(main); @@ -202,8 +225,9 @@ public static UserInfo signIn(Main main, @Nonnull String email, } } - public static UserInfo signIn(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, @Nonnull String email, - @Nonnull String password) + public static AuthRecipeUserInfo signIn(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, + @Nonnull String email, + @Nonnull String password) throws StorageQueryException, WrongCredentialsException, TenantOrAppNotFoundException, BadPermissionException { @@ -215,8 +239,19 @@ public static UserInfo signIn(TenantIdentifierWithStorage tenantIdentifierWithSt throw new BadPermissionException("Email password login not enabled for tenant"); } - UserInfo user = tenantIdentifierWithStorage.getEmailPasswordStorage() - .getUserInfoUsingEmail(tenantIdentifierWithStorage, email); + AuthRecipeUserInfo[] users = tenantIdentifierWithStorage.getAuthRecipeStorage() + .listPrimaryUsersByEmail(tenantIdentifierWithStorage, email); + + AuthRecipeUserInfo user = null; + LoginMethod lM = null; + for (AuthRecipeUserInfo currUser : users) { + for (LoginMethod currLM : currUser.loginMethods) { + if (currLM.recipeId == RECIPE_ID.EMAIL_PASSWORD && currLM.email.equals(email) && currLM.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { + user = currUser; + lM = currLM; + } + } + } if (user == null) { throw new WrongCredentialsException(); @@ -224,7 +259,8 @@ public static UserInfo signIn(TenantIdentifierWithStorage tenantIdentifierWithSt try { if (!PasswordHashing.getInstance(main) - .verifyPasswordWithHash(tenantIdentifierWithStorage.toAppIdentifier(), password, user.passwordHash)) { + .verifyPasswordWithHash(tenantIdentifierWithStorage.toAppIdentifier(), password, + lM.passwordHash)) { throw new WrongCredentialsException(); } } catch (WrongCredentialsException e) { @@ -243,20 +279,68 @@ public static UserInfo signIn(TenantIdentifierWithStorage tenantIdentifierWithSt } @TestOnly - public static String generatePasswordResetToken(Main main, String userId) + public static String generatePasswordResetTokenBeforeCdi4_0(Main main, String userId) throws InvalidKeySpecException, NoSuchAlgorithmException, StorageQueryException, UnknownUserIdException { try { Storage storage = StorageLayer.getStorage(main); - return generatePasswordResetToken( + return generatePasswordResetTokenBeforeCdi4_0( new TenantIdentifierWithStorage(null, null, null, storage), main, userId); + } catch (TenantOrAppNotFoundException | BadPermissionException | WebserverAPI.BadRequestException e) { + throw new IllegalStateException(e); + } + } + + @TestOnly + public static String generatePasswordResetTokenBeforeCdi4_0WithoutAddingEmail(Main main, String userId) + throws InvalidKeySpecException, NoSuchAlgorithmException, StorageQueryException, UnknownUserIdException { + try { + Storage storage = StorageLayer.getStorage(main); + return generatePasswordResetToken( + new TenantIdentifierWithStorage(null, null, null, storage), + main, userId, null); } catch (TenantOrAppNotFoundException | BadPermissionException e) { throw new IllegalStateException(e); } } + @TestOnly + public static String generatePasswordResetToken(Main main, String userId, String email) + throws InvalidKeySpecException, NoSuchAlgorithmException, StorageQueryException, UnknownUserIdException { + try { + Storage storage = StorageLayer.getStorage(main); + return generatePasswordResetToken( + new TenantIdentifierWithStorage(null, null, null, storage), + main, userId, email); + } catch (TenantOrAppNotFoundException | BadPermissionException e) { + throw new IllegalStateException(e); + } + } + + public static String generatePasswordResetTokenBeforeCdi4_0(TenantIdentifierWithStorage tenantIdentifierWithStorage, + Main main, + String userId) + throws InvalidKeySpecException, NoSuchAlgorithmException, StorageQueryException, UnknownUserIdException, + TenantOrAppNotFoundException, BadPermissionException, WebserverAPI.BadRequestException { + AppIdentifierWithStorage appIdentifierWithStorage = + tenantIdentifierWithStorage.toAppIdentifierWithStorage(); + AuthRecipeUserInfo user = AuthRecipe.getUserById(appIdentifierWithStorage, userId); + if (user == null) { + throw new UnknownUserIdException(); + } + if (user.loginMethods.length > 1) { + throw new WebserverAPI.BadRequestException("Please use CDI version >= 4.0"); + } + if (user.loginMethods[0].email == null || + user.loginMethods[0].recipeId != RECIPE_ID.EMAIL_PASSWORD) { + // this used to be the behaviour of the older CDI version and it was enforced via a fkey constraint + throw new UnknownUserIdException(); + } + return generatePasswordResetToken(tenantIdentifierWithStorage, main, userId, user.loginMethods[0].email); + } + public static String generatePasswordResetToken(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, - String userId) + String userId, String email) throws InvalidKeySpecException, NoSuchAlgorithmException, StorageQueryException, UnknownUserIdException, TenantOrAppNotFoundException, BadPermissionException { @@ -293,7 +377,7 @@ public static String generatePasswordResetToken(TenantIdentifierWithStorage tena tenantIdentifierWithStorage.getEmailPasswordStorage().addPasswordResetToken( tenantIdentifierWithStorage.toAppIdentifier(), new PasswordResetTokenInfo(userId, hashedToken, System.currentTimeMillis() + - getPasswordResetTokenLifetime(tenantIdentifierWithStorage, main))); + getPasswordResetTokenLifetime(tenantIdentifierWithStorage, main), email)); return token; } catch (DuplicatePasswordResetTokenException ignored) { } @@ -301,6 +385,7 @@ public static String generatePasswordResetToken(TenantIdentifierWithStorage tena } @TestOnly + @Deprecated public static String resetPassword(Main main, String token, String password) throws ResetPasswordInvalidTokenException, NoSuchAlgorithmException, StorageQueryException, @@ -314,6 +399,7 @@ public static String resetPassword(Main main, String token, } } + @Deprecated public static String resetPassword(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String token, String password) throws ResetPasswordInvalidTokenException, NoSuchAlgorithmException, StorageQueryException, @@ -351,7 +437,8 @@ public static String resetPassword(TenantIdentifierWithStorage tenantIdentifierW throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException()); } - storage.deleteAllPasswordResetTokensForUser_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + storage.deleteAllPasswordResetTokensForUser_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), + con, userId); if (matchedToken.tokenExpiry < System.currentTimeMillis()) { @@ -373,12 +460,109 @@ public static String resetPassword(TenantIdentifierWithStorage tenantIdentifierW } } + @TestOnly + public static ConsumeResetPasswordTokenResult consumeResetPasswordToken(Main main, String token) + throws ResetPasswordInvalidTokenException, NoSuchAlgorithmException, StorageQueryException, + StorageTransactionLogicException { + try { + Storage storage = StorageLayer.getStorage(main); + return consumeResetPasswordToken(new TenantIdentifierWithStorage(null, null, null, storage), + token); + } catch (TenantOrAppNotFoundException e) { + throw new IllegalStateException(e); + } + } + + public static class ConsumeResetPasswordTokenResult { + public String userId; + public String email; + + public ConsumeResetPasswordTokenResult(String userId, String email) { + this.userId = userId; + this.email = email; + } + } + + public static ConsumeResetPasswordTokenResult consumeResetPasswordToken( + TenantIdentifierWithStorage tenantIdentifierWithStorage, String token) + throws ResetPasswordInvalidTokenException, NoSuchAlgorithmException, StorageQueryException, + StorageTransactionLogicException, TenantOrAppNotFoundException { + String hashedToken = Utils.hashSHA256(token); + + EmailPasswordSQLStorage storage = tenantIdentifierWithStorage.getEmailPasswordStorage(); + + PasswordResetTokenInfo resetInfo = storage.getPasswordResetTokenInfo( + tenantIdentifierWithStorage.toAppIdentifier(), hashedToken); + + if (resetInfo == null) { + throw new ResetPasswordInvalidTokenException(); + } + + final String userId = resetInfo.userId; + + try { + return storage.startTransaction(con -> { + + PasswordResetTokenInfo[] allTokens = storage.getAllPasswordResetTokenInfoForUser_Transaction( + tenantIdentifierWithStorage.toAppIdentifier(), con, + userId); + + PasswordResetTokenInfo matchedToken = null; + for (PasswordResetTokenInfo tok : allTokens) { + if (tok.token.equals(hashedToken)) { + matchedToken = tok; + break; + } + } + + if (matchedToken == null) { + throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException()); + } + + storage.deleteAllPasswordResetTokensForUser_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), + con, + userId); + + if (matchedToken.tokenExpiry < System.currentTimeMillis()) { + storage.commitTransaction(con); + throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException()); + } + + storage.commitTransaction(con); + if (matchedToken.email == null) { + // this is possible if the token was generated before migration, and then consumed + // after migration + AppIdentifierWithStorage appIdentifierWithStorage = + tenantIdentifierWithStorage.toAppIdentifierWithStorage(); + AuthRecipeUserInfo user = AuthRecipe.getUserById(appIdentifierWithStorage, userId); + if (user == null) { + throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException()); + } + if (user.loginMethods.length > 1) { + throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException()); + } + if (user.loginMethods[0].email == null || + user.loginMethods[0].recipeId != RECIPE_ID.EMAIL_PASSWORD) { + throw new StorageTransactionLogicException(new ResetPasswordInvalidTokenException()); + } + return new ConsumeResetPasswordTokenResult(matchedToken.userId, user.loginMethods[0].email); + } + return new ConsumeResetPasswordTokenResult(matchedToken.userId, matchedToken.email); + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof ResetPasswordInvalidTokenException) { + throw (ResetPasswordInvalidTokenException) e.actualException; + } + throw e; + } + } + @TestOnly public static void updateUsersEmailOrPassword(Main main, @Nonnull String userId, @Nullable String email, @Nullable String password) throws StorageQueryException, StorageTransactionLogicException, - UnknownUserIdException, DuplicateEmailException { + UnknownUserIdException, DuplicateEmailException, EmailChangeNotAllowedException { try { Storage storage = StorageLayer.getStorage(main); updateUsersEmailOrPassword(new AppIdentifierWithStorage(null, null, storage), @@ -392,18 +576,50 @@ public static void updateUsersEmailOrPassword(AppIdentifierWithStorage appIdenti @Nonnull String userId, @Nullable String email, @Nullable String password) throws StorageQueryException, StorageTransactionLogicException, - UnknownUserIdException, DuplicateEmailException, TenantOrAppNotFoundException { + UnknownUserIdException, DuplicateEmailException, TenantOrAppNotFoundException, + EmailChangeNotAllowedException { EmailPasswordSQLStorage storage = appIdentifierWithStorage.getEmailPasswordStorage(); + AuthRecipeSQLStorage authRecipeStorage = (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage(); try { storage.startTransaction(transaction -> { try { - UserInfo userInfo = storage.getUserInfoUsingId_Transaction(appIdentifierWithStorage, transaction, userId); + AuthRecipeUserInfo user = authRecipeStorage.getPrimaryUserById_Transaction(appIdentifierWithStorage, + transaction, userId); - if (userInfo == null) { + if (user == null) { + throw new StorageTransactionLogicException(new UnknownUserIdException()); + } + boolean foundEmailPasswordLoginMethod = false; + for (LoginMethod lm : user.loginMethods) { + if (lm.recipeId == RECIPE_ID.EMAIL_PASSWORD && lm.getSupertokensUserId().equals(userId)) { + foundEmailPasswordLoginMethod = true; + break; + } + } + if (!foundEmailPasswordLoginMethod) { throw new StorageTransactionLogicException(new UnknownUserIdException()); } if (email != null) { + if (user.isPrimaryUser) { + for (String tenantId : user.tenantIds) { + AuthRecipeUserInfo[] existingUsersWithNewEmail = + authRecipeStorage.listPrimaryUsersByEmail_Transaction( + appIdentifierWithStorage, transaction, + email); + + for (AuthRecipeUserInfo userWithSameEmail : existingUsersWithNewEmail) { + if (!userWithSameEmail.tenantIds.contains(tenantId)) { + continue; + } + if (userWithSameEmail.isPrimaryUser && !userWithSameEmail.getSupertokensUserId().equals(user.getSupertokensUserId())) { + throw new StorageTransactionLogicException( + new EmailChangeNotAllowedException()); + } + } + } + } + try { storage.updateUsersEmail_Transaction(appIdentifierWithStorage, transaction, userId, email); @@ -415,7 +631,8 @@ public static void updateUsersEmailOrPassword(AppIdentifierWithStorage appIdenti if (password != null) { String hashedPassword = PasswordHashing.getInstance(main) .createHashWithSalt(appIdentifierWithStorage, password); - storage.updateUsersPassword_Transaction(appIdentifierWithStorage, transaction, userId, hashedPassword); + storage.updateUsersPassword_Transaction(appIdentifierWithStorage, transaction, userId, + hashedPassword); } storage.commitTransaction(transaction); @@ -431,13 +648,16 @@ public static void updateUsersEmailOrPassword(AppIdentifierWithStorage appIdenti throw (DuplicateEmailException) e.actualException; } else if (e.actualException instanceof TenantOrAppNotFoundException) { throw (TenantOrAppNotFoundException) e.actualException; + } else if (e.actualException instanceof EmailChangeNotAllowedException) { + throw (EmailChangeNotAllowedException) e.actualException; } throw e; } } + @Deprecated @TestOnly - public static UserInfo getUserUsingId(Main main, String userId) + public static AuthRecipeUserInfo getUserUsingId(Main main, String userId) throws StorageQueryException { try { Storage storage = StorageLayer.getStorage(main); @@ -447,14 +667,36 @@ public static UserInfo getUserUsingId(Main main, String userId) } } - public static UserInfo getUserUsingId(AppIdentifierWithStorage appIdentifierWithStorage, String userId) + @Deprecated + public static AuthRecipeUserInfo getUserUsingId(AppIdentifierWithStorage appIdentifierWithStorage, String userId) throws StorageQueryException, TenantOrAppNotFoundException { - return appIdentifierWithStorage.getEmailPasswordStorage().getUserInfoUsingId(appIdentifierWithStorage, userId); + AuthRecipeUserInfo result = appIdentifierWithStorage.getAuthRecipeStorage() + .getPrimaryUserById(appIdentifierWithStorage, userId); + if (result == null) { + return null; + } + for (LoginMethod lM : result.loginMethods) { + if (lM.getSupertokensUserId().equals(userId) && lM.recipeId == RECIPE_ID.EMAIL_PASSWORD) { + return AuthRecipeUserInfo.create(lM.getSupertokensUserId(), result.isPrimaryUser, lM); + } + } + return null; } - public static UserInfo getUserUsingEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage, String email) + @Deprecated + public static AuthRecipeUserInfo getUserUsingEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage, + String email) throws StorageQueryException, TenantOrAppNotFoundException { - return tenantIdentifierWithStorage.getEmailPasswordStorage().getUserInfoUsingEmail( + AuthRecipeUserInfo[] users = tenantIdentifierWithStorage.getAuthRecipeStorage().listPrimaryUsersByEmail( tenantIdentifierWithStorage, email); + // filter used based on login method + for (AuthRecipeUserInfo user : users) { + for (LoginMethod lM : user.loginMethods) { + if (lM.email.equals(email) && lM.recipeId == RECIPE_ID.EMAIL_PASSWORD) { + return user; + } + } + } + return null; } } diff --git a/src/main/java/io/supertokens/emailpassword/exceptions/EmailChangeNotAllowedException.java b/src/main/java/io/supertokens/emailpassword/exceptions/EmailChangeNotAllowedException.java new file mode 100644 index 000000000..e8660b592 --- /dev/null +++ b/src/main/java/io/supertokens/emailpassword/exceptions/EmailChangeNotAllowedException.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.emailpassword.exceptions; + +public class EmailChangeNotAllowedException extends Exception { + private static final long serialVersionUID = -7205953190075543040L; +} diff --git a/src/main/java/io/supertokens/exceptions/TokenTheftDetectedException.java b/src/main/java/io/supertokens/exceptions/TokenTheftDetectedException.java index a4c2ddfef..700793cf7 100644 --- a/src/main/java/io/supertokens/exceptions/TokenTheftDetectedException.java +++ b/src/main/java/io/supertokens/exceptions/TokenTheftDetectedException.java @@ -21,10 +21,12 @@ public class TokenTheftDetectedException extends Exception { private static final long serialVersionUID = -7964000536695705071L; public final String sessionHandle; - public final String userId; + public final String recipeUserId; + public final String primaryUserId; - public TokenTheftDetectedException(String sessionHandle, String userId) { + public TokenTheftDetectedException(String sessionHandle, String recipeUserId, String primaryUserId) { this.sessionHandle = sessionHandle; - this.userId = userId; + this.recipeUserId = recipeUserId; + this.primaryUserId = primaryUserId; } } diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 43be8c364..f05d07f8a 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -24,13 +24,14 @@ import io.supertokens.inmemorydb.queries.*; import io.supertokens.pluginInterface.*; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.pluginInterface.dashboard.DashboardSessionInfo; import io.supertokens.pluginInterface.dashboard.DashboardUser; import io.supertokens.pluginInterface.dashboard.exceptions.UserIdNotFoundException; import io.supertokens.pluginInterface.dashboard.sqlStorage.DashboardSQLStorage; import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo; -import io.supertokens.pluginInterface.emailpassword.UserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicatePasswordResetTokenException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException; @@ -53,6 +54,7 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; import io.supertokens.pluginInterface.passwordless.exception.*; @@ -75,6 +77,7 @@ import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage; import io.supertokens.pluginInterface.useridmapping.exception.UnknownSuperTokensUserIdException; import io.supertokens.pluginInterface.useridmapping.exception.UserIdMappingAlreadyExistsException; +import io.supertokens.pluginInterface.useridmapping.sqlStorage.UserIdMappingSQLStorage; import io.supertokens.pluginInterface.usermetadata.UserMetadataStorage; import io.supertokens.pluginInterface.usermetadata.sqlStorage.UserMetadataSQLStorage; import io.supertokens.pluginInterface.userroles.UserRolesStorage; @@ -90,12 +93,16 @@ import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLTransactionRollbackException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, - MultitenancyStorage, TOTPSQLStorage, ActiveUsersStorage, DashboardSQLStorage { + UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, TOTPSQLStorage, ActiveUsersStorage, + DashboardSQLStorage, AuthRecipeSQLStorage { private static final Object appenderLock = new Object(); private static final String APP_ID_KEY_NAME = "app_id"; @@ -134,7 +141,8 @@ public STORAGE_TYPE getType() { } @Override - public void loadConfig(JsonObject ignored, Set logLevel, TenantIdentifier tenantIdentifier) throws InvalidConfigException { + public void loadConfig(JsonObject ignored, Set logLevel, TenantIdentifier tenantIdentifier) + throws InvalidConfigException { Config.loadConfig(this); } @@ -248,7 +256,8 @@ public KeyValueInfo getLegacyAccessTokenSigningKey_Transaction(AppIdentifier app throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return GeneralQueries.getKeyValue_Transaction(this, sqlCon, appIdentifier.getAsPublicTenantIdentifier(), ACCESS_TOKEN_SIGNING_KEY_NAME); + return GeneralQueries.getKeyValue_Transaction(this, sqlCon, appIdentifier.getAsPublicTenantIdentifier(), + ACCESS_TOKEN_SIGNING_KEY_NAME); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -448,7 +457,7 @@ public KeyValueInfo getKeyValue(TenantIdentifier tenantIdentifier, String key) t } } - @Override + @Override public void setKeyValue(TenantIdentifier tenantIdentifier, String key, KeyValueInfo info) throws StorageQueryException, TenantOrAppNotFoundException { try { @@ -514,7 +523,19 @@ public void updateSessionInfo_Transaction(TenantIdentifier tenantIdentifier, Tra long expiry) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - SessionQueries.updateSessionInfo_Transaction(this, sqlCon, tenantIdentifier, sessionHandle, refreshTokenHash2, expiry); + SessionQueries.updateSessionInfo_Transaction(this, sqlCon, tenantIdentifier, sessionHandle, + refreshTokenHash2, expiry); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void deleteSessionsOfUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + Connection sqlCon = (Connection) con.getConnection(); + try { + SessionQueries.deleteSessionsOfUser_Transaction(sqlCon, this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -602,7 +623,8 @@ public boolean isUserIdBeingUsedInNonAuthRecipe(AppIdentifier appIdentifier, Str @TestOnly @Override - public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifier, String className, String userId) throws StorageQueryException { + public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifier, String className, String userId) + throws StorageQueryException { if (!isTesting) { throw new UnsupportedOperationException(); } @@ -672,7 +694,8 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi try { long now = System.currentTimeMillis(); TOTPQueries.insertUsedCode_Transaction(this, - (Connection) con.getConnection(), tenantIdentifier, new TOTPUsedCode(userId, "123456", true, 1000+now, now)); + (Connection) con.getConnection(), tenantIdentifier, + new TOTPUsedCode(userId, "123456", true, 1000 + now, now)); } catch (SQLException e) { throw new StorageTransactionLogicException(e); } @@ -706,7 +729,8 @@ public String[] getProtectedConfigsFromSuperTokensSaaSUsers() { } @Override - public UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String email, String passwordHash, long timeJoined) + public AuthRecipeUserInfo signUp(TenantIdentifier tenantIdentifier, String id, String email, String passwordHash, + long timeJoined) throws StorageQueryException, DuplicateUserIdException, DuplicateEmailException, TenantOrAppNotFoundException { try { @@ -716,12 +740,17 @@ public UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String emai SQLiteConfig config = Config.getConfig(this); String serverMessage = eTemp.actualException.getMessage(); - if (isUniqueConstraintError(serverMessage, config.getEmailPasswordUserToTenantTable(), new String[]{"app_id", "tenant_id", "email"})) { + if (isUniqueConstraintError(serverMessage, config.getEmailPasswordUserToTenantTable(), + new String[]{"app_id", "tenant_id", "email"})) { throw new DuplicateEmailException(); - } else if (isPrimaryKeyError(serverMessage, config.getEmailPasswordUsersTable(), new String[]{"app_id", "user_id"}) - || isPrimaryKeyError(serverMessage, config.getUsersTable(), new String[]{"app_id", "tenant_id", "user_id"}) - || isPrimaryKeyError(serverMessage, config.getEmailPasswordUserToTenantTable(), new String[]{"app_id", "tenant_id", "user_id"}) - || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(), new String[]{"app_id", "user_id"})) { + } else if (isPrimaryKeyError(serverMessage, config.getEmailPasswordUsersTable(), + new String[]{"app_id", "user_id"}) + || isPrimaryKeyError(serverMessage, config.getUsersTable(), + new String[]{"app_id", "tenant_id", "user_id"}) + || isPrimaryKeyError(serverMessage, config.getEmailPasswordUserToTenantTable(), + new String[]{"app_id", "tenant_id", "user_id"}) + || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(), + new String[]{"app_id", "user_id"})) { throw new DuplicateUserIdException(); } else if (isForeignKeyConstraintError( serverMessage, @@ -742,45 +771,18 @@ public UserInfo signUp(TenantIdentifier tenantIdentifier, String id, String emai } } - @Override - public void deleteEmailPasswordUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { - try { - EmailPasswordQueries.deleteUser(this, appIdentifier, userId); - } catch (StorageTransactionLogicException e) { - throw new StorageQueryException(e.actualException); - } - } - - @Override - public UserInfo getUserInfoUsingId(AppIdentifier appIdentifier, String id) throws StorageQueryException { - try { - return EmailPasswordQueries.getUserInfoUsingId(this, appIdentifier, id); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public UserInfo getUserInfoUsingEmail(TenantIdentifier tenantIdentifier, String email) - throws StorageQueryException { - try { - return EmailPasswordQueries.getUserInfoUsingEmail(this, tenantIdentifier, email); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public void addPasswordResetToken(AppIdentifier appIdentifier, PasswordResetTokenInfo passwordResetTokenInfo) throws StorageQueryException, UnknownUserIdException, DuplicatePasswordResetTokenException { try { EmailPasswordQueries.addPasswordResetToken(this, appIdentifier, passwordResetTokenInfo.userId, - passwordResetTokenInfo.token, passwordResetTokenInfo.tokenExpiry); + passwordResetTokenInfo.token, passwordResetTokenInfo.tokenExpiry, passwordResetTokenInfo.email); } catch (SQLException e) { if (e instanceof SQLiteException) { String serverMessage = e.getMessage(); - if (isPrimaryKeyError(serverMessage, Config.getConfig(this).getPasswordResetTokensTable(), new String[]{"app_id", "user_id", "token"})) { + if (isPrimaryKeyError(serverMessage, Config.getConfig(this).getPasswordResetTokensTable(), + new String[]{"app_id", "user_id", "token"})) { throw new DuplicatePasswordResetTokenException(); } else if (isForeignKeyConstraintError( serverMessage, @@ -861,7 +863,8 @@ public void updateUsersEmail_Transaction(AppIdentifier appIdentifier, Transactio EmailPasswordQueries.updateUsersEmail_Transaction(this, sqlCon, appIdentifier, userId, email); } catch (SQLException e) { if (isUniqueConstraintError(e.getMessage(), - Config.getConfig(this).getEmailPasswordUserToTenantTable(), new String[]{"app_id", "tenant_id", "email"})) { + Config.getConfig(this).getEmailPasswordUserToTenantTable(), + new String[]{"app_id", "tenant_id", "email"})) { throw new DuplicateEmailException(); } throw new StorageQueryException(e); @@ -869,12 +872,12 @@ public void updateUsersEmail_Transaction(AppIdentifier appIdentifier, Transactio } @Override - public UserInfo getUserInfoUsingId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, - String userId) + public void deleteEmailPasswordUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) throws StorageQueryException { - Connection sqlCon = (Connection) con.getConnection(); try { - return EmailPasswordQueries.getUserInfoUsingId_Transaction(this, sqlCon, appIdentifier, userId); + Connection sqlCon = (Connection) con.getConnection(); + EmailPasswordQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -953,12 +956,13 @@ public void updateIsEmailVerified_Transaction(AppIdentifier appIdentifier, Trans } @Override - public void deleteEmailVerificationUserInfo(AppIdentifier appIdentifier, String userId) - throws StorageQueryException { + public void deleteEmailVerificationUserInfo_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String userId) throws StorageQueryException { try { - EmailVerificationQueries.deleteUserInfo(this, appIdentifier, userId); - } catch (StorageTransactionLogicException e) { - throw new StorageQueryException(e.actualException); + Connection sqlCon = (Connection) con.getConnection(); + EmailVerificationQueries.deleteUserInfo_Transaction(sqlCon, this, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); } } @@ -984,7 +988,8 @@ public void addEmailVerificationToken(TenantIdentifier tenantIdentifier, SQLiteConfig config = Config.getConfig(this); String serverMessage = e.getMessage(); - if (isPrimaryKeyError(serverMessage, config.getEmailVerificationTokensTable(), new String[]{"app_id", "tenant_id", "user_id", "email", "token"})) { + if (isPrimaryKeyError(serverMessage, config.getEmailVerificationTokensTable(), + new String[]{"app_id", "tenant_id", "user_id", "email", "token"})) { throw new DuplicateEmailVerificationTokenException(); } @@ -1063,37 +1068,34 @@ public void deleteExpiredPasswordResetTokens() throws StorageQueryException { } @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getUserInfoUsingId_Transaction( - AppIdentifier appIdentifier, TransactionConnection con, - String thirdPartyId, - String thirdPartyUserId) - throws StorageQueryException { + public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String thirdPartyId, String thirdPartyUserId, + String newEmail) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return ThirdPartyQueries.getUserInfoUsingId_Transaction(this, sqlCon, appIdentifier, thirdPartyId, - thirdPartyUserId); + ThirdPartyQueries.updateUserEmail_Transaction(this, sqlCon, appIdentifier, thirdPartyId, + thirdPartyUserId, newEmail); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, - String thirdPartyId, String thirdPartyUserId, - String newEmail) throws StorageQueryException { - Connection sqlCon = (Connection) con.getConnection(); + public void deleteThirdPartyUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, + boolean deleteUserIdMappingToo) + throws StorageQueryException { try { - ThirdPartyQueries.updateUserEmail_Transaction(this, sqlCon, appIdentifier, thirdPartyId, - thirdPartyUserId, newEmail); + Connection sqlCon = (Connection) con.getConnection(); + ThirdPartyQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo signUp( + public AuthRecipeUserInfo signUp( TenantIdentifier tenantIdentifier, String id, String email, - io.supertokens.pluginInterface.thirdparty.UserInfo.ThirdParty thirdParty, long timeJoined) + LoginMethod.ThirdParty thirdParty, long timeJoined) throws StorageQueryException, io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException, DuplicateThirdPartyUserException, TenantOrAppNotFoundException { try { @@ -1107,10 +1109,14 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo signUp( new String[]{"app_id", "tenant_id", "third_party_id", "third_party_user_id"})) { throw new DuplicateThirdPartyUserException(); - } else if (isPrimaryKeyError(serverMessage, config.getThirdPartyUsersTable(), new String[]{"app_id", "user_id"}) - || isPrimaryKeyError(serverMessage, config.getUsersTable(), new String[]{"app_id", "tenant_id", "user_id"}) - || isPrimaryKeyError(serverMessage, config.getThirdPartyUserToTenantTable(), new String[]{"app_id", "tenant_id", "user_id"}) - || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(), new String[]{"app_id", "user_id"})) { + } else if (isPrimaryKeyError(serverMessage, config.getThirdPartyUsersTable(), + new String[]{"app_id", "user_id"}) + || isPrimaryKeyError(serverMessage, config.getUsersTable(), + new String[]{"app_id", "tenant_id", "user_id"}) + || isPrimaryKeyError(serverMessage, config.getThirdPartyUserToTenantTable(), + new String[]{"app_id", "tenant_id", "user_id"}) + || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(), + new String[]{"app_id", "user_id"})) { throw new io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException(); } else if (isForeignKeyConstraintError( @@ -1134,143 +1140,162 @@ public io.supertokens.pluginInterface.thirdparty.UserInfo signUp( } @Override - public void deleteThirdPartyUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public long getUsersCount(TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds) + throws StorageQueryException { try { - ThirdPartyQueries.deleteUser(this, appIdentifier, userId); - } catch (StorageTransactionLogicException e) { - throw new StorageQueryException(e.actualException); + return GeneralQueries.getUsersCount(this, tenantIdentifier, includeRecipeIds); + } catch (SQLException e) { + throw new StorageQueryException(e); } } @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId( - TenantIdentifier tenantIdentifier, String thirdPartyId, - String thirdPartyUserId) + public long getUsersCount(AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds) throws StorageQueryException { try { - return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, tenantIdentifier, thirdPartyId, - thirdPartyUserId); + return GeneralQueries.getUsersCount(this, appIdentifier, includeRecipeIds); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo getThirdPartyUserInfoUsingId(AppIdentifier appIdentifier, - String id) + public AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @NotNull Integer limit, + @NotNull String timeJoinedOrder, + @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, + @Nullable Long timeJoined, @Nullable DashboardSearchTags dashboardSearchTags) throws StorageQueryException { try { - return ThirdPartyQueries.getThirdPartyUserInfoUsingId(this, appIdentifier, id); + return GeneralQueries.getUsers(this, tenantIdentifier, limit, timeJoinedOrder, includeRecipeIds, userId, + timeJoined, dashboardSearchTags); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public io.supertokens.pluginInterface.thirdparty.UserInfo[] getThirdPartyUsersByEmail( - TenantIdentifier tenantIdentifier, @NotNull String email) - throws StorageQueryException { + public boolean doesUserIdExist(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - return ThirdPartyQueries.getThirdPartyUsersByEmail(this, tenantIdentifier, email); + return GeneralQueries.doesUserIdExist(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public long getUsersCount(TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds) - throws StorageQueryException { + public boolean doesUserIdExist_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - return GeneralQueries.getUsersCount(this, tenantIdentifier, includeRecipeIds); + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.doesUserIdExist_Transaction(this, sqlCon, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public long getUsersCount(AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds) - throws StorageQueryException { + public void updateLastActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - return GeneralQueries.getUsersCount(this, appIdentifier, includeRecipeIds); + ActiveUsersQueries.updateUserLastActive(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public AuthRecipeUserInfo[] getUsers(TenantIdentifier tenantIdentifier, @NotNull Integer limit, - @NotNull String timeJoinedOrder, - @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, - @Nullable Long timeJoined, @Nullable DashboardSearchTags dashboardSearchTags) - throws StorageQueryException { + public int countUsersActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException { try { - return GeneralQueries.getUsers(this, tenantIdentifier, limit, timeJoinedOrder, includeRecipeIds, userId, - timeJoined, dashboardSearchTags); + return ActiveUsersQueries.countUsersActiveSince(this, appIdentifier, time); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public boolean doesUserIdExist(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public int countUsersEnabledTotp(AppIdentifier appIdentifier) throws StorageQueryException { try { - return GeneralQueries.doesUserIdExist(this, appIdentifier, userId); + return ActiveUsersQueries.countUsersEnabledTotp(this, appIdentifier); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public void updateLastActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long time) + throws StorageQueryException { try { - ActiveUsersQueries.updateUserLastActive(this, appIdentifier, userId); + return ActiveUsersQueries.countUsersEnabledTotpAndActiveSince(this, appIdentifier, time); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public int countUsersActiveSince(AppIdentifier appIdentifier, long time) throws StorageQueryException { + public void deleteUserActive_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { try { - return ActiveUsersQueries.countUsersActiveSince(this, appIdentifier, time); + Connection sqlCon = (Connection) con.getConnection(); + ActiveUsersQueries.deleteUserActive_Transaction(sqlCon, this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public int countUsersEnabledTotp(AppIdentifier appIdentifier) throws StorageQueryException { + public boolean doesUserIdExist(TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException { try { - return ActiveUsersQueries.countUsersEnabledTotp(this, appIdentifier); + return GeneralQueries.doesUserIdExist(this, tenantIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long time) + public AuthRecipeUserInfo getPrimaryUserById(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - return ActiveUsersQueries.countUsersEnabledTotpAndActiveSince(this, appIdentifier, time); + return GeneralQueries.getPrimaryUserInfoForUserId(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public void deleteUserActive(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public String getPrimaryUserIdStrForUserId(AppIdentifier appIdentifier, String userId) + throws StorageQueryException { try { - ActiveUsersQueries.deleteUserActive(this, appIdentifier, userId); + return GeneralQueries.getPrimaryUserIdStrForUserId(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public boolean doesUserIdExist(TenantIdentifier tenantIdentifier, String userId) + public AuthRecipeUserInfo[] listPrimaryUsersByEmail(TenantIdentifier tenantIdentifier, String email) throws StorageQueryException { try { - return GeneralQueries.doesUserIdExist(this, tenantIdentifier, userId); + return GeneralQueries.listPrimaryUsersByEmail(this, tenantIdentifier, email); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber) + throws StorageQueryException { + try { + return GeneralQueries.listPrimaryUsersByPhoneNumber(this, tenantIdentifier, phoneNumber); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo(TenantIdentifier tenantIdentifier, String thirdPartyId, + String thirdPartyUserId) throws StorageQueryException { + try { + return GeneralQueries.getPrimaryUserByThirdPartyInfo(this, tenantIdentifier, thirdPartyId, + thirdPartyUserId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1300,7 +1325,8 @@ public void setJWTSigningKey_Transaction(AppIdentifier appIdentifier, Transactio SQLiteConfig config = Config.getConfig(this); String serverMessage = e.getMessage(); - if (isPrimaryKeyError(serverMessage, config.getJWTSigningKeysTable(), new String[]{"app_id", "key_id"})) { + if (isPrimaryKeyError(serverMessage, config.getJWTSigningKeysTable(), + new String[]{"app_id", "key_id"})) { throw new DuplicateKeyIdException(); } @@ -1322,7 +1348,8 @@ private boolean isUniqueConstraintError(String serverMessage, String tableName, return isPrimaryKeyError(serverMessage, tableName, columnNames); } - private boolean isForeignKeyConstraintError(String serverMessage, String tableName, String[] columnNames, Object[] values) { + private boolean isForeignKeyConstraintError(String serverMessage, String tableName, String[] columnNames, + Object[] values) { if (!serverMessage.contains("FOREIGN KEY constraint failed")) { return false; } @@ -1496,7 +1523,8 @@ public void deleteCode_Transaction(TenantIdentifier tenantIdentifier, Transactio } @Override - public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, String email) + public void updateUserEmail_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + String email) throws StorageQueryException, UnknownUserIdException, DuplicateEmailException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -1508,7 +1536,8 @@ public void updateUserEmail_Transaction(AppIdentifier appIdentifier, Transaction } catch (SQLException e) { if (e instanceof SQLiteException) { if (isUniqueConstraintError(e.getMessage(), - Config.getConfig(this).getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "email"})) { + Config.getConfig(this).getPasswordlessUserToTenantTable(), + new String[]{"app_id", "tenant_id", "email"})) { throw new DuplicateEmailException(); } } @@ -1533,7 +1562,8 @@ public void updateUserPhoneNumber_Transaction(AppIdentifier appIdentifier, Trans } catch (SQLException e) { if (e instanceof SQLiteException) { if (isUniqueConstraintError(e.getMessage(), - Config.getConfig(this).getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "phone_number"})) { + Config.getConfig(this).getPasswordlessUserToTenantTable(), + new String[]{"app_id", "tenant_id", "phone_number"})) { throw new DuplicatePhoneNumberException(); } } @@ -1542,6 +1572,18 @@ public void updateUserPhoneNumber_Transaction(AppIdentifier appIdentifier, Trans } } + @Override + public void deletePasswordlessUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + PasswordlessQueries.deleteUser_Transaction(sqlCon, this, appIdentifier, userId, deleteUserIdMappingToo); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable String email, @Nullable String phoneNumber, @NotNull String linkCodeSalt, @@ -1559,13 +1601,16 @@ public void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable St String serverMessage = e.actualException.getMessage(); SQLiteConfig config = Config.getConfig(this); - if (isPrimaryKeyError(serverMessage, config.getPasswordlessDevicesTable(), new String[]{"app_id", "tenant_id", "device_id_hash"})) { + if (isPrimaryKeyError(serverMessage, config.getPasswordlessDevicesTable(), + new String[]{"app_id", "tenant_id", "device_id_hash"})) { throw new DuplicateDeviceIdHashException(); } - if (isPrimaryKeyError(serverMessage, config.getPasswordlessCodesTable(), new String[]{"app_id", "tenant_id", "code_id"})) { + if (isPrimaryKeyError(serverMessage, config.getPasswordlessCodesTable(), + new String[]{"app_id", "tenant_id", "code_id"})) { throw new DuplicateCodeIdException(); } - if (isUniqueConstraintError(serverMessage, config.getPasswordlessCodesTable(), new String[]{"app_id", "tenant_id", "link_code_hash"})) { + if (isUniqueConstraintError(serverMessage, config.getPasswordlessCodesTable(), + new String[]{"app_id", "tenant_id", "link_code_hash"})) { throw new DuplicateLinkCodeHashException(); } if (isForeignKeyConstraintError( @@ -1582,7 +1627,8 @@ public void createDeviceWithCode(TenantIdentifier tenantIdentifier, @Nullable St } @Override - public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) throws StorageQueryException, UnknownDeviceIdHash, + public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) + throws StorageQueryException, UnknownDeviceIdHash, DuplicateCodeIdException, DuplicateLinkCodeHashException { try { @@ -1614,9 +1660,11 @@ public void createCode(TenantIdentifier tenantIdentifier, PasswordlessCode code) } @Override - public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIdentifier tenantIdentifier, - String id, @javax.annotation.Nullable String email, - @javax.annotation.Nullable String phoneNumber, long timeJoined) + public AuthRecipeUserInfo createUser(TenantIdentifier tenantIdentifier, + String id, + @javax.annotation.Nullable String email, + @javax.annotation.Nullable + String phoneNumber, long timeJoined) throws StorageQueryException, DuplicateEmailException, DuplicatePhoneNumberException, DuplicateUserIdException, TenantOrAppNotFoundException { @@ -1627,10 +1675,14 @@ public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIde SQLiteConfig config = Config.getConfig(this); String serverMessage = e.actualException.getMessage(); - if (isPrimaryKeyError(serverMessage, config.getPasswordlessUsersTable(), new String[]{"app_id", "user_id"}) - || isPrimaryKeyError(serverMessage, config.getUsersTable(), new String[]{"app_id", "tenant_id", "user_id"}) - || isPrimaryKeyError(serverMessage, config.getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "user_id"}) - || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(), new String[]{"app_id", "user_id"})) { + if (isPrimaryKeyError(serverMessage, config.getPasswordlessUsersTable(), + new String[]{"app_id", "user_id"}) + || isPrimaryKeyError(serverMessage, config.getUsersTable(), + new String[]{"app_id", "tenant_id", "user_id"}) + || isPrimaryKeyError(serverMessage, config.getPasswordlessUserToTenantTable(), + new String[]{"app_id", "tenant_id", "user_id"}) + || isPrimaryKeyError(serverMessage, config.getAppIdToUserIdTable(), + new String[]{"app_id", "user_id"})) { throw new DuplicateUserIdException(); } @@ -1640,7 +1692,8 @@ public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIde } if (isUniqueConstraintError(serverMessage, - config.getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "phone_number"})) { + config.getPasswordlessUserToTenantTable(), + new String[]{"app_id", "tenant_id", "phone_number"})) { throw new DuplicatePhoneNumberException(); } @@ -1665,16 +1718,6 @@ public io.supertokens.pluginInterface.passwordless.UserInfo createUser(TenantIde } } - @Override - public void deletePasswordlessUser(AppIdentifier appIdentifier, String userId) throws - StorageQueryException { - try { - PasswordlessQueries.deleteUser(this, appIdentifier, userId); - } catch (StorageTransactionLogicException e) { - throw new StorageQueryException(e.actualException); - } - } - @Override public PasswordlessDevice getDevice(TenantIdentifier tenantIdentifier, String deviceIdHash) throws StorageQueryException { @@ -1745,36 +1788,6 @@ public PasswordlessCode getCodeByLinkCodeHash(TenantIdentifier tenantIdentifier, } } - @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserById(AppIdentifier appIdentifier, String userId) - throws StorageQueryException { - try { - return PasswordlessQueries.getUserById(this, appIdentifier, userId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserByEmail(TenantIdentifier tenantIdentifier, String email) - throws StorageQueryException { - try { - return PasswordlessQueries.getUserByEmail(this, tenantIdentifier, email); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public io.supertokens.pluginInterface.passwordless.UserInfo getUserByPhoneNumber(TenantIdentifier tenantIdentifier, String phoneNumber) - throws StorageQueryException { - try { - return PasswordlessQueries.getUserByPhoneNumber(this, tenantIdentifier, phoneNumber); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public JsonObject getUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { @@ -1797,7 +1810,8 @@ public JsonObject getUserMetadata_Transaction(AppIdentifier appIdentifier, Trans } @Override - public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, JsonObject metadata) + public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + JsonObject metadata) throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -1820,6 +1834,17 @@ public int setUserMetadata_Transaction(AppIdentifier appIdentifier, TransactionC } } + @Override + public int deleteUserMetadata_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return UserMetadataQueries.deleteUserMetadata_Transaction(sqlCon, this, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public int deleteUserMetadata(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { @@ -1834,7 +1859,7 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri throws StorageQueryException, UnknownRoleException, DuplicateUserRoleMappingException, TenantOrAppNotFoundException { try { - UserRoleQueries.addRoleToUser(this, tenantIdentifier, userId, role); + UserRolesQueries.addRoleToUser(this, tenantIdentifier, userId, role); } catch (SQLException e) { if (e instanceof SQLiteException) { SQLiteConfig config = Config.getConfig(this); @@ -1847,7 +1872,8 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri new Object[]{tenantIdentifier.getAppId(), role})) { throw new UnknownRoleException(); } - if (isPrimaryKeyError(serverErrorMessage, config.getUserRolesTable(), new String[]{"app_id", "tenant_id", "user_id", "role"})) { + if (isPrimaryKeyError(serverErrorMessage, config.getUserRolesTable(), + new String[]{"app_id", "tenant_id", "user_id", "role"})) { throw new DuplicateUserRoleMappingException(); } if (isForeignKeyConstraintError( @@ -1867,7 +1893,7 @@ public void addRoleToUser(TenantIdentifier tenantIdentifier, String userId, Stri public String[] getRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { try { - return UserRoleQueries.getRolesForUser(this, tenantIdentifier, userId); + return UserRolesQueries.getRolesForUser(this, tenantIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1876,7 +1902,7 @@ public String[] getRolesForUser(TenantIdentifier tenantIdentifier, String userId private String[] getRolesForUser(AppIdentifier appIdentifier, String userId) throws StorageQueryException { try { - return UserRoleQueries.getRolesForUser(this, appIdentifier, userId); + return UserRolesQueries.getRolesForUser(this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1886,7 +1912,7 @@ private String[] getRolesForUser(AppIdentifier appIdentifier, String userId) thr public String[] getUsersForRole(TenantIdentifier tenantIdentifier, String role) throws StorageQueryException { try { - return UserRoleQueries.getUsersForRole(this, tenantIdentifier, role); + return UserRolesQueries.getUsersForRole(this, tenantIdentifier, role); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1896,7 +1922,7 @@ public String[] getUsersForRole(TenantIdentifier tenantIdentifier, String role) public String[] getPermissionsForRole(AppIdentifier appIdentifier, String role) throws StorageQueryException { try { - return UserRoleQueries.getPermissionsForRole(this, appIdentifier, role); + return UserRolesQueries.getPermissionsForRole(this, appIdentifier, role); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1906,7 +1932,7 @@ public String[] getPermissionsForRole(AppIdentifier appIdentifier, String role) public String[] getRolesThatHavePermission(AppIdentifier appIdentifier, String permission) throws StorageQueryException { try { - return UserRoleQueries.getRolesThatHavePermission(this, appIdentifier, permission); + return UserRolesQueries.getRolesThatHavePermission(this, appIdentifier, permission); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1915,7 +1941,7 @@ public String[] getRolesThatHavePermission(AppIdentifier appIdentifier, String p @Override public boolean deleteRole(AppIdentifier appIdentifier, String role) throws StorageQueryException { try { - return UserRoleQueries.deleteRole(this, appIdentifier, role); + return UserRolesQueries.deleteRole(this, appIdentifier, role); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1924,7 +1950,7 @@ public boolean deleteRole(AppIdentifier appIdentifier, String role) throws Stora @Override public String[] getRoles(AppIdentifier appIdentifier) throws StorageQueryException { try { - return UserRoleQueries.getRoles(this, appIdentifier); + return UserRolesQueries.getRoles(this, appIdentifier); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1933,7 +1959,7 @@ public String[] getRoles(AppIdentifier appIdentifier) throws StorageQueryExcepti @Override public boolean doesRoleExist(AppIdentifier appIdentifier, String role) throws StorageQueryException { try { - return UserRoleQueries.doesRoleExist(this, appIdentifier, role); + return UserRolesQueries.doesRoleExist(this, appIdentifier, role); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -1943,28 +1969,19 @@ public boolean doesRoleExist(AppIdentifier appIdentifier, String role) throws St public int deleteAllRolesForUser(TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException { try { - return UserRoleQueries.deleteAllRolesForUser(this, tenantIdentifier, userId); + return UserRolesQueries.deleteAllRolesForUser(this, tenantIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } } @Override - public void deleteAllRolesForUser(AppIdentifier appIdentifier, String userId) throws - StorageQueryException { - try { - UserRoleQueries.deleteAllRolesForUser(this, appIdentifier, userId); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public boolean deleteRoleForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId, String role) + public boolean deleteRoleForUser_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, + String userId, String role) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return UserRoleQueries.deleteRoleForUser_Transaction(this, sqlCon, tenantIdentifier, userId, + return UserRolesQueries.deleteRoleForUser_Transaction(this, sqlCon, tenantIdentifier, userId, role); } catch (SQLException e) { throw new StorageQueryException(e); @@ -1977,7 +1994,7 @@ public boolean createNewRoleOrDoNothingIfExists_Transaction(AppIdentifier appIde throws StorageQueryException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { - return UserRoleQueries.createNewRoleOrDoNothingIfExists_Transaction( + return UserRolesQueries.createNewRoleOrDoNothingIfExists_Transaction( this, sqlCon, appIdentifier, role); } catch (SQLException e) { if (e instanceof SQLiteException) { @@ -2003,7 +2020,7 @@ public void addPermissionToRoleOrDoNothingIfExists_Transaction(AppIdentifier app throws StorageQueryException, UnknownRoleException { Connection sqlCon = (Connection) con.getConnection(); try { - UserRoleQueries.addPermissionToRoleOrDoNothingIfExists_Transaction(this, sqlCon, appIdentifier, + UserRolesQueries.addPermissionToRoleOrDoNothingIfExists_Transaction(this, sqlCon, appIdentifier, role, permission); } catch (SQLException e) { if (e instanceof SQLiteException) { @@ -2022,11 +2039,12 @@ public void addPermissionToRoleOrDoNothingIfExists_Transaction(AppIdentifier app } @Override - public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role, String permission) + public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String role, String permission) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return UserRoleQueries.deletePermissionForRole_Transaction(this, sqlCon, appIdentifier, role, + return UserRolesQueries.deletePermissionForRole_Transaction(this, sqlCon, appIdentifier, role, permission); } catch (SQLException e) { throw new StorageQueryException(e); @@ -2034,11 +2052,12 @@ public boolean deletePermissionForRole_Transaction(AppIdentifier appIdentifier, } @Override - public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) + public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String role) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return UserRoleQueries.deleteAllPermissionsForRole_Transaction(this, sqlCon, appIdentifier, + return UserRolesQueries.deleteAllPermissionsForRole_Transaction(this, sqlCon, appIdentifier, role); } catch (SQLException e) { throw new StorageQueryException(e); @@ -2046,10 +2065,22 @@ public int deleteAllPermissionsForRole_Transaction(AppIdentifier appIdentifier, } @Override - public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) throws StorageQueryException { + public boolean doesRoleExist_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String role) + throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { - return UserRoleQueries.doesRoleExist_transaction(this, sqlCon, appIdentifier, role); + return UserRolesQueries.doesRoleExist_transaction(this, sqlCon, appIdentifier, role); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void deleteAllRolesForUser_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + UserRolesQueries.deleteAllRolesForUser_Transaction(sqlCon, this, appIdentifier, userId); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2075,7 +2106,8 @@ public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensU throw new UnknownSuperTokensUserIdException(); } - if (isPrimaryKeyError(serverErrorMessage, config.getUserIdMappingTable(), new String[]{"app_id", "supertokens_user_id", "external_user_id"})) { + if (isPrimaryKeyError(serverErrorMessage, config.getUserIdMappingTable(), + new String[]{"app_id", "supertokens_user_id", "external_user_id"})) { throw new UserIdMappingAlreadyExistsException(true, true); } @@ -2095,7 +2127,8 @@ public void createUserIdMapping(AppIdentifier appIdentifier, String superTokensU } @Override - public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) throws StorageQueryException { + public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) + throws StorageQueryException { try { if (isSuperTokensUserId) { return UserIdMappingQueries.deleteUserIdMappingWithSuperTokensUserId(this, appIdentifier, @@ -2110,7 +2143,8 @@ public boolean deleteUserIdMapping(AppIdentifier appIdentifier, String userId, b } @Override - public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) throws StorageQueryException { + public UserIdMapping getUserIdMapping(AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) + throws StorageQueryException { try { if (isSuperTokensUserId) { return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId(this, appIdentifier, @@ -2173,13 +2207,17 @@ public void createTenant(TenantConfig tenantConfig) if (e.actualException instanceof SQLiteException) { String errorMessage = e.actualException.getMessage(); SQLiteConfig config = Config.getConfig(this); - if (isPrimaryKeyError(errorMessage, config.getTenantConfigsTable(), new String[]{"connection_uri_domain", "app_id", "tenant_id"})) { + if (isPrimaryKeyError(errorMessage, config.getTenantConfigsTable(), + new String[]{"connection_uri_domain", "app_id", "tenant_id"})) { throw new DuplicateTenantException(); } - if (isPrimaryKeyError(errorMessage, config.getTenantThirdPartyProvidersTable(), new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id"})) { + if (isPrimaryKeyError(errorMessage, config.getTenantThirdPartyProvidersTable(), + new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id"})) { throw new DuplicateThirdPartyIdException(); } - if (isPrimaryKeyError(errorMessage, config.getTenantThirdPartyProviderClientsTable(), new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id", "client_type"})) { + if (isPrimaryKeyError(errorMessage, config.getTenantThirdPartyProviderClientsTable(), + new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id", + "client_type"})) { throw new DuplicateClientTypeException(); } } @@ -2195,7 +2233,8 @@ public void addTenantIdInTargetStorage(TenantIdentifier tenantIdentifier) } catch (StorageTransactionLogicException e) { if (e.actualException instanceof SQLiteException) { String errorMessage = e.actualException.getMessage(); - if (isPrimaryKeyError(errorMessage, Config.getConfig(this).getTenantsTable(), new String[]{"app_id", "tenant_id"})) { + if (isPrimaryKeyError(errorMessage, Config.getConfig(this).getTenantsTable(), + new String[]{"app_id", "tenant_id"})) { throw new DuplicateTenantException(); } } @@ -2216,12 +2255,15 @@ public void overwriteTenantConfig(TenantConfig tenantConfig) if (e.actualException instanceof SQLiteException) { SQLiteConfig config = Config.getConfig(this); if (isPrimaryKeyError(e.actualException.getMessage(), - config.getTenantThirdPartyProvidersTable(), new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id"})) { + config.getTenantThirdPartyProvidersTable(), + new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id"})) { throw new DuplicateThirdPartyIdException(); } if (isPrimaryKeyError(e.actualException.getMessage(), - config.getTenantThirdPartyProviderClientsTable(), new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id", "client_type"})) { + config.getTenantThirdPartyProviderClientsTable(), + new String[]{"connection_uri_domain", "app_id", "tenant_id", "third_party_id", + "client_type"})) { throw new DuplicateClientTypeException(); } } @@ -2255,71 +2297,64 @@ public TenantConfig[] getAllTenants() throws StorageQueryException { } @Override - public boolean addUserIdToTenant(TenantIdentifier tenantIdentifier, String userId) - throws TenantOrAppNotFoundException, UnknownUserIdException, StorageQueryException, - DuplicateEmailException, DuplicateThirdPartyUserException, DuplicatePhoneNumberException { + public boolean addUserIdToTenant_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String userId) + throws StorageQueryException, TenantOrAppNotFoundException, DuplicateEmailException, + DuplicateThirdPartyUserException, DuplicatePhoneNumberException, UnknownUserIdException { + Connection sqlCon = (Connection) con.getConnection(); try { - return this.startTransaction(con -> { - Connection sqlCon = (Connection) con.getConnection(); - try { - String recipeId = GeneralQueries.getRecipeIdForUser_Transaction(this, sqlCon, tenantIdentifier, - userId); - - if (recipeId == null) { - throw new StorageTransactionLogicException(new UnknownUserIdException()); - } - - boolean added; - if (recipeId.equals("emailpassword")) { - added = EmailPasswordQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, userId); - } else if (recipeId.equals("thirdparty")) { - added = ThirdPartyQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, userId); - } else if (recipeId.equals("passwordless")) { - added = PasswordlessQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, userId); - } else { - throw new IllegalStateException("Should never come here!"); - } + String recipeId = GeneralQueries.getRecipeIdForUser_Transaction(this, sqlCon, tenantIdentifier, + userId); - sqlCon.commit(); - return added; - } catch (SQLException throwables) { - throw new StorageTransactionLogicException(throwables); - } - }); - } catch (StorageTransactionLogicException e) { - if (e.actualException instanceof SQLException) { - SQLiteConfig config = Config.getConfig(this); - String serverErrorMessage = e.actualException.getMessage(); + if (recipeId == null) { + throw new UnknownUserIdException(); + } - if (isForeignKeyConstraintError( - serverErrorMessage, - config.getTenantsTable(), - new String[]{"app_id", "tenant_id"}, - new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) { - throw new TenantOrAppNotFoundException(tenantIdentifier); - } - if (isUniqueConstraintError(serverErrorMessage, config.getEmailPasswordUserToTenantTable(), new String[]{"app_id", "tenant_id", "email"})) { - throw new DuplicateEmailException(); - } - if (isUniqueConstraintError(serverErrorMessage, config.getThirdPartyUserToTenantTable(), new String[]{"app_id", "tenant_id", "third_party_id", "third_party_user_id"})) { - throw new DuplicateThirdPartyUserException(); - } - if (isUniqueConstraintError(serverErrorMessage, - Config.getConfig(this).getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "phone_number"})) { - throw new DuplicatePhoneNumberException(); - } - if (isUniqueConstraintError(serverErrorMessage, - Config.getConfig(this).getPasswordlessUserToTenantTable(), new String[]{"app_id", "tenant_id", "email"})) { - throw new DuplicateEmailException(); - } + boolean added; + if (recipeId.equals("emailpassword")) { + added = EmailPasswordQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, + userId); + } else if (recipeId.equals("thirdparty")) { + added = ThirdPartyQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + } else if (recipeId.equals("passwordless")) { + added = PasswordlessQueries.addUserIdToTenant_Transaction(this, sqlCon, tenantIdentifier, + userId); + } else { + throw new IllegalStateException("Should never come here!"); + } - throw new StorageQueryException(e.actualException); - } else if (e.actualException instanceof UnknownUserIdException) { - throw (UnknownUserIdException) e.actualException; - } else if (e.actualException instanceof StorageQueryException) { - throw (StorageQueryException) e.actualException; + sqlCon.commit(); + return added; + } catch (SQLException throwables) { + SQLiteConfig config = Config.getConfig(this); + String serverErrorMessage = throwables.getMessage(); + + if (isForeignKeyConstraintError( + serverErrorMessage, + config.getTenantsTable(), + new String[]{"app_id", "tenant_id"}, + new Object[]{tenantIdentifier.getAppId(), tenantIdentifier.getTenantId()})) { + throw new TenantOrAppNotFoundException(tenantIdentifier); } - throw new StorageQueryException(e.actualException); + if (isUniqueConstraintError(serverErrorMessage, config.getEmailPasswordUserToTenantTable(), + new String[]{"app_id", "tenant_id", "email"})) { + throw new DuplicateEmailException(); + } + if (isUniqueConstraintError(serverErrorMessage, config.getThirdPartyUserToTenantTable(), + new String[]{"app_id", "tenant_id", "third_party_id", "third_party_user_id"})) { + throw new DuplicateThirdPartyUserException(); + } + if (isUniqueConstraintError(serverErrorMessage, + Config.getConfig(this).getPasswordlessUserToTenantTable(), + new String[]{"app_id", "tenant_id", "phone_number"})) { + throw new DuplicatePhoneNumberException(); + } + if (isUniqueConstraintError(serverErrorMessage, + Config.getConfig(this).getPasswordlessUserToTenantTable(), + new String[]{"app_id", "tenant_id", "email"})) { + throw new DuplicateEmailException(); + } + + throw new StorageQueryException(throwables); } } @@ -2340,11 +2375,14 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String boolean removed; if (recipeId.equals("emailpassword")) { - removed = EmailPasswordQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + removed = EmailPasswordQueries.removeUserIdFromTenant_Transaction(this, sqlCon, + tenantIdentifier, userId); } else if (recipeId.equals("thirdparty")) { - removed = ThirdPartyQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + removed = ThirdPartyQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, + userId); } else if (recipeId.equals("passwordless")) { - removed = PasswordlessQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, userId); + removed = PasswordlessQueries.removeUserIdFromTenant_Transaction(this, sqlCon, tenantIdentifier, + userId); } else { throw new IllegalStateException("Should never come here!"); } @@ -2366,7 +2404,8 @@ public boolean removeUserIdFromTenant(TenantIdentifier tenantIdentifier, String } @Override - public boolean deleteDashboardUserWithUserId(AppIdentifier appIdentifier, String userId) throws StorageQueryException { + public boolean deleteDashboardUserWithUserId(AppIdentifier appIdentifier, String userId) + throws StorageQueryException { try { return DashboardQueries.deleteDashboardUserWithUserId(this, appIdentifier, userId); } catch (SQLException e) { @@ -2414,7 +2453,8 @@ public DashboardSessionInfo getSessionInfoWithSessionId(AppIdentifier appIdentif } @Override - public boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String sessionId) throws StorageQueryException { + public boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String sessionId) + throws StorageQueryException { try { return DashboardQueries.deleteDashboardUserSessionWithSessionId(this, appIdentifier, sessionId); @@ -2424,7 +2464,8 @@ public boolean revokeSessionWithSessionId(AppIdentifier appIdentifier, String se } @Override - public void updateDashboardUsersEmailWithUserId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId, + public void updateDashboardUsersEmailWithUserId_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String userId, String newEmail) throws StorageQueryException, io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException, UserIdNotFoundException { Connection sqlCon = (Connection) con.getConnection(); @@ -2497,7 +2538,8 @@ public void createNewDashboardUser(AppIdentifier appIdentifier, DashboardUser us SQLiteConfig config = Config.getConfig(this); String serverErrorMessage = e.getMessage(); - if (isPrimaryKeyError(serverErrorMessage, config.getDashboardUsersTable(), new String[]{"app_id", "user_id"})) { + if (isPrimaryKeyError(serverErrorMessage, config.getDashboardUsersTable(), + new String[]{"app_id", "user_id"})) { throw new io.supertokens.pluginInterface.dashboard.exceptions.DuplicateUserIdException(); } if (isUniqueConstraintError(serverErrorMessage, config.getDashboardUsersTable(), @@ -2546,7 +2588,8 @@ public void createDevice(AppIdentifier appIdentifier, TOTPDevice device) if (e.actualException instanceof SQLiteException) { String errMsg = e.actualException.getMessage(); - if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable(), new String[]{"app_id", "user_id", "device_name"})) { + if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable(), + new String[]{"app_id", "user_id", "device_name"})) { throw new DeviceAlreadyExistsException(); } else if (isForeignKeyConstraintError( errMsg, @@ -2621,11 +2664,12 @@ public void updateDeviceName(AppIdentifier appIdentifier, String userId, String } catch (SQLException e) { if (e instanceof SQLiteException) { String errMsg = e.getMessage(); - if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable(), new String[]{"app_id", "user_id", "device_name"})) { + if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable(), + new String[]{"app_id", "user_id", "device_name"})) { throw new DeviceAlreadyExistsException(); } } - throw new StorageQueryException(e); + throw new StorageQueryException(e); } } @@ -2659,7 +2703,8 @@ public void insertUsedCode_Transaction(TransactionConnection con, TenantIdentifi try { TOTPQueries.insertUsedCode_Transaction(this, sqlCon, tenantIdentifier, usedCodeObj); } catch (SQLException e) { - if (isPrimaryKeyError(e.getMessage(), Config.getConfig(this).getTotpUsedCodesTable(), new String[]{"app_id", "tenant_id", "user_id", "created_time_ms"})) { + if (isPrimaryKeyError(e.getMessage(), Config.getConfig(this).getTotpUsedCodesTable(), + new String[]{"app_id", "tenant_id", "user_id", "created_time_ms"})) { throw new UsedCodeAlreadyExistsException(); } else if (isForeignKeyConstraintError( @@ -2738,4 +2783,168 @@ public String[] getAllTablesInTheDatabaseThatHasDataForAppId(String appId) throw throw new StorageQueryException(e); } } + + @Override + public AuthRecipeUserInfo getPrimaryUserById_Transaction(AppIdentifier appIdentifier, TransactionConnection con, + String userId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.getPrimaryUserInfoForUserId_Transaction(this, sqlCon, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, String email) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.listPrimaryUsersByEmail_Transaction(this, sqlCon, appIdentifier, email); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, + String phoneNumber) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.listPrimaryUsersByPhoneNumber_Transaction(this, sqlCon, appIdentifier, + phoneNumber); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo(AppIdentifier appIdentifier, + String thirdPartyId, + String thirdPartyUserId) + throws StorageQueryException { + try { + return GeneralQueries.listPrimaryUsersByThirdPartyInfo(this, appIdentifier, + thirdPartyId, thirdPartyUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, + String thirdPartyId, + String thirdPartyUserId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return GeneralQueries.listPrimaryUsersByThirdPartyInfo_Transaction(this, sqlCon, appIdentifier, + thirdPartyId, thirdPartyUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void makePrimaryUser_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String userId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + // we do not bother returning if a row was updated here or not, cause it's happening + // in a transaction anyway. + GeneralQueries.makePrimaryUser_Transaction(this, sqlCon, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void linkAccounts_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String recipeUserId, + String primaryUserId) throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + // we do not bother returning if a row was updated here or not, cause it's happening + // in a transaction anyway. + GeneralQueries.linkAccounts_Transaction(this, sqlCon, appIdentifier, recipeUserId, primaryUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public void unlinkAccounts_Transaction(AppIdentifier appIdentifier, TransactionConnection con, String primaryUserId, String recipeUserId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + // we do not bother returning if a row was updated here or not, cause it's happening + // in a transaction anyway. + GeneralQueries.unlinkAccounts_Transaction(this, sqlCon, appIdentifier, primaryUserId, recipeUserId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public boolean checkIfUsesAccountLinking(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return GeneralQueries.checkIfUsesAccountLinking(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(AppIdentifier appIdentifier, long sinceTime) throws StorageQueryException { + try { + return ActiveUsersQueries.countUsersActiveSinceAndHasMoreThanOneLoginMethod(this, appIdentifier, sinceTime); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int getUsersCountWithMoreThanOneLoginMethod(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return GeneralQueries.getUsersCountWithMoreThanOneLoginMethod(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + + @Override + public UserIdMapping getUserIdMapping_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, boolean isSuperTokensUserId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + if (isSuperTokensUserId) { + return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId_Transaction(this, sqlCon, appIdentifier, + userId); + } + + return UserIdMappingQueries.getUserIdMappingWithExternalUserId_Transaction(this, sqlCon, appIdentifier, userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public UserIdMapping[] getUserIdMapping_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) + throws StorageQueryException { + try { + Connection sqlCon = (Connection) con.getConnection(); + return UserIdMappingQueries.getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId_Transaction(this, + sqlCon, + appIdentifier, + userId); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + } diff --git a/src/main/java/io/supertokens/inmemorydb/Utils.java b/src/main/java/io/supertokens/inmemorydb/Utils.java new file mode 100644 index 000000000..ce8bfa367 --- /dev/null +++ b/src/main/java/io/supertokens/inmemorydb/Utils.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.inmemorydb; + +public class Utils { + public static String generateCommaSeperatedQuestionMarks(int size) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < size; i++) { + builder.append("?"); + if (i != size - 1) { + builder.append(","); + } + } + return builder.toString(); + } +} diff --git a/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java index 7ca5d26b1..48aacda21 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java @@ -1,5 +1,6 @@ package io.supertokens.inmemorydb.queries; +import java.sql.Connection; import java.sql.SQLException; import io.supertokens.inmemorydb.config.Config; @@ -22,7 +23,8 @@ static String getQueryToCreateUserLastActiveTable(Start start) { + " );"; } - public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException { + public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) + throws SQLException, StorageQueryException { String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getUserLastActiveTable() + " WHERE app_id = ? AND last_active_time >= ?"; @@ -37,7 +39,30 @@ public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier }); } - public static int countUsersEnabledTotp(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { + public static int countUsersActiveSinceAndHasMoreThanOneLoginMethod(Start start, AppIdentifier appIdentifier, long sinceTime) + throws SQLException, StorageQueryException { + String QUERY = "SELECT count(1) as c FROM (" + + " SELECT count(user_id) as num_login_methods, app_id, primary_or_recipe_user_id" + + " FROM " + Config.getConfig(start).getUsersTable() + + " WHERE primary_or_recipe_user_id IN (" + + " SELECT user_id FROM " + Config.getConfig(start).getUserLastActiveTable() + + " WHERE app_id = ? AND last_active_time >= ?" + + " )" + + " GROUP BY app_id, primary_or_recipe_user_id" + + ") uc WHERE num_login_methods > 1"; + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setLong(2, sinceTime); + }, result -> { + if (result.next()) { + return result.getInt("c"); + } + return 0; + }); + } + + public static int countUsersEnabledTotp(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getTotpUsersTable() + " WHERE app_id = ?"; @@ -51,8 +76,10 @@ public static int countUsersEnabledTotp(Start start, AppIdentifier appIdentifier }); } - public static int countUsersEnabledTotpAndActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException { - String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getTotpUsersTable() + " AS totp_users " + public static int countUsersEnabledTotpAndActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) + throws SQLException, StorageQueryException { + String QUERY = + "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getTotpUsersTable() + " AS totp_users " + "INNER JOIN " + Config.getConfig(start).getUserLastActiveTable() + " AS user_last_active " + "ON totp_users.user_id = user_last_active.user_id " + "WHERE user_last_active.app_id = ? AND user_last_active.last_active_time >= ?"; @@ -68,9 +95,12 @@ public static int countUsersEnabledTotpAndActiveSince(Start start, AppIdentifier }); } - public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getUserLastActiveTable() - + "(app_id, user_id, last_active_time) VALUES(?, ?, ?) ON CONFLICT(app_id, user_id) DO UPDATE SET last_active_time = ?"; + + + "(app_id, user_id, last_active_time) VALUES(?, ?, ?) ON CONFLICT(app_id, user_id) DO UPDATE SET " + + "last_active_time = ?"; long now = System.currentTimeMillis(); return update(start, QUERY, pst -> { @@ -101,12 +131,13 @@ public static Long getLastActiveByUserId(Start start, AppIdentifier appIdentifie } } - public static void deleteUserActive(Start start, AppIdentifier appIdentifier, String userId) + public static void deleteUserActive_Transaction(Connection con, Start start, AppIdentifier appIdentifier, + String userId) throws StorageQueryException, SQLException { String QUERY = "DELETE FROM " + Config.getConfig(start).getUserLastActiveTable() + " WHERE app_id = ? AND user_id = ?"; - update(start, QUERY, pst -> { + update(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); }); diff --git a/src/main/java/io/supertokens/inmemorydb/queries/DashboardQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/DashboardQueries.java index 587bfd610..425768e39 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/DashboardQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/DashboardQueries.java @@ -37,9 +37,9 @@ public class DashboardQueries { public static String getQueryToCreateDashboardUsersTable(Start start) { - String tableName = Config.getConfig(start).getDashboardUsersTable(); + String dashboardUsersTable = Config.getConfig(start).getDashboardUsersTable(); // @formatter:off - return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + return "CREATE TABLE IF NOT EXISTS " + dashboardUsersTable + " (" + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," + "email VARCHAR(256) NOT NULL," @@ -287,5 +287,4 @@ public DashboardSessionInfo[] extract(ResultSet result) throws SQLException, Sto return temp.toArray(DashboardSessionInfo[]::new); } } - -} \ No newline at end of file +} diff --git a/src/main/java/io/supertokens/inmemorydb/queries/EmailPasswordQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/EmailPasswordQueries.java index 18649b2c7..fb27d870f 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/EmailPasswordQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/EmailPasswordQueries.java @@ -16,13 +16,15 @@ package io.supertokens.inmemorydb.queries; -import io.supertokens.inmemorydb.ConnectionPool; import io.supertokens.inmemorydb.ConnectionWithLocks; import io.supertokens.inmemorydb.Start; +import io.supertokens.inmemorydb.Utils; import io.supertokens.inmemorydb.config.Config; import io.supertokens.pluginInterface.RowMapper; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; @@ -32,6 +34,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute; import static io.supertokens.inmemorydb.QueryExecutorTemplate.update; @@ -50,7 +53,8 @@ static String getQueryToCreateUsersTable(Start start) { + "time_joined BIGINT UNSIGNED NOT NULL," + "PRIMARY KEY (app_id, user_id)," + "FOREIGN KEY(app_id, user_id)" - + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + + " (app_id, user_id) ON DELETE CASCADE" + ");"; } @@ -65,7 +69,8 @@ static String getQueryToCreateEmailPasswordUserToTenantTable(Start start) { + "UNIQUE (app_id, tenant_id, email)," + "PRIMARY KEY (app_id, tenant_id, user_id)," + "FOREIGN KEY (app_id, tenant_id, user_id)" - + " REFERENCES " + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getUsersTable() + + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -75,9 +80,10 @@ static String getQueryToCreatePasswordResetTokensTable(Start start) { + "app_id VARCHAR(64) DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," + "token VARCHAR(128) NOT NULL UNIQUE," + + "email VARCHAR(256)," // nullable cause of backwards compatibility. + "token_expiry BIGINT UNSIGNED NOT NULL," + "PRIMARY KEY (app_id, user_id, token)," - + "FOREIGN KEY (app_id, user_id) REFERENCES " + Config.getConfig(start).getEmailPasswordUsersTable() + + "FOREIGN KEY (app_id, user_id) REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE ON UPDATE CASCADE" + ");"; } @@ -93,7 +99,8 @@ public static void deleteExpiredPasswordResetTokens(Start start) throws SQLExcep update(start, QUERY, pst -> pst.setLong(1, currentTimeMillis())); } - public static void updateUsersPassword_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String newPassword) + public static void updateUsersPassword_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String userId, String newPassword) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getEmailPasswordUsersTable() + " SET password_hash = ? WHERE app_id = ? AND user_id = ?"; @@ -105,7 +112,8 @@ public static void updateUsersPassword_Transaction(Start start, Connection con, }); } - public static void updateUsersEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String newEmail) + public static void updateUsersEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String userId, String newEmail) throws SQLException, StorageQueryException { { String QUERY = "UPDATE " + getConfig(start).getEmailPasswordUsersTable() @@ -129,10 +137,12 @@ public static void updateUsersEmail_Transaction(Start start, Connection con, App } } - public static void deleteAllPasswordResetTokensForUser_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId) + public static void deleteAllPasswordResetTokensForUser_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - String QUERY = "DELETE FROM " + getConfig(start).getPasswordResetTokensTable() + " WHERE app_id = ? AND user_id = ?"; + String QUERY = + "DELETE FROM " + getConfig(start).getPasswordResetTokensTable() + " WHERE app_id = ? AND user_id = ?"; update(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -143,7 +153,8 @@ public static void deleteAllPasswordResetTokensForUser_Transaction(Start start, public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser(Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { - String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable() + String QUERY = + "SELECT user_id, token, token_expiry, email FROM " + getConfig(start).getPasswordResetTokensTable() + " WHERE app_id = ? AND user_id = ?"; return execute(start, QUERY, pst -> { @@ -167,11 +178,13 @@ public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Trans String userId) throws SQLException, StorageQueryException { - ((ConnectionWithLocks) con).lock(appIdentifier.getAppId() + "~" + userId + Config.getConfig(start).getPasswordResetTokensTable()); + ((ConnectionWithLocks) con).lock( + appIdentifier.getAppId() + "~" + userId + Config.getConfig(start).getPasswordResetTokensTable()); - String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable() - + " WHERE app_id = ? AND user_id = ?"; + String QUERY = + "SELECT user_id, token, token_expiry, email FROM " + getConfig(start).getPasswordResetTokensTable() + + " WHERE app_id = ? AND user_id = ?"; return execute(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -189,30 +202,11 @@ public static PasswordResetTokenInfo[] getAllPasswordResetTokenInfoForUser_Trans }); } - public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, AppIdentifier appIdentifier, - String id) - throws SQLException, StorageQueryException { - - ((ConnectionWithLocks) con).lock(appIdentifier.getAppId() + "~" + id + Config.getConfig(start).getEmailPasswordUsersTable()); - - String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " - + getConfig(start).getEmailPasswordUsersTable() - + " WHERE app_id = ? AND user_id = ?"; - UserInfoPartial userInfo = execute(con, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, id); - }, result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); - } - return null; - }); - return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfo); - } - - public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, AppIdentifier appIdentifier, String token) + public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, AppIdentifier appIdentifier, + String token) throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id, token, token_expiry FROM " + getConfig(start).getPasswordResetTokensTable() + String QUERY = + "SELECT user_id, token, token_expiry, email FROM " + getConfig(start).getPasswordResetTokensTable() + " WHERE app_id = ? AND token = ?"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -225,43 +219,62 @@ public static PasswordResetTokenInfo getPasswordResetTokenInfo(Start start, AppI }); } - public static void addPasswordResetToken(Start start, AppIdentifier appIdentifier, String userId, String tokenHash, long expiry) + public static void addPasswordResetToken(Start start, AppIdentifier appIdentifier, String userId, String tokenHash, + long expiry, String email) throws SQLException, StorageQueryException { - String QUERY = "INSERT INTO " + getConfig(start).getPasswordResetTokensTable() - + "(app_id, user_id, token, token_expiry)" + " VALUES(?, ?, ?, ?)"; + if (email != null) { + String QUERY = "INSERT INTO " + getConfig(start).getPasswordResetTokensTable() + + "(app_id, user_id, token, token_expiry, email)" + " VALUES(?, ?, ?, ?, ?)"; - update(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - pst.setString(3, tokenHash); - pst.setLong(4, expiry); - }); + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, tokenHash); + pst.setLong(4, expiry); + pst.setString(5, email); + }); + } else { + String QUERY = "INSERT INTO " + getConfig(start).getPasswordResetTokensTable() + + "(app_id, user_id, token, token_expiry)" + " VALUES(?, ?, ?, ?)"; + + update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, tokenHash); + pst.setLong(4, expiry); + }); + } } - public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String userId, String email, String passwordHash, long timeJoined) + public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String userId, String email, + String passwordHash, long timeJoined) throws StorageQueryException, StorageTransactionLogicException { return start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { { // app_id_to_user_id String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() - + "(app_id, user_id, recipe_id)" + " VALUES(?, ?, ?)"; + + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, userId); - pst.setString(3, EMAIL_PASSWORD.toString()); + pst.setString(3, userId); + pst.setString(4, EMAIL_PASSWORD.toString()); }); } { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, userId); - pst.setString(4, EMAIL_PASSWORD.toString()); - pst.setLong(5, timeJoined); + pst.setString(4, userId); + pst.setString(5, EMAIL_PASSWORD.toString()); + pst.setLong(6, timeJoined); + pst.setLong(7, timeJoined); }); } @@ -290,57 +303,62 @@ public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, St }); } - UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), new UserInfoPartial(userId, email, passwordHash, timeJoined)); - + UserInfoPartial userInfo = new UserInfoPartial(userId, email, passwordHash, timeJoined); + fillUserInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); + fillUserInfoWithVerified_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); sqlCon.commit(); - return userInfo; + return AuthRecipeUserInfo.create(userId, false, userInfo.toLoginMethod()); } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); } }); } - public static void deleteUser(Start start, AppIdentifier appIdentifier, String userId) - throws StorageQueryException, StorageTransactionLogicException { - start.startTransaction(con -> { - Connection sqlCon = (Connection) con.getConnection(); - try { - { - String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() - + " WHERE app_id = ? AND user_id = ?"; - - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }); - } + public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) + throws StorageQueryException, SQLException { + if (deleteUserIdMappingToo) { + String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ?"; - sqlCon.commit(); - } catch (SQLException throwables) { - throw new StorageTransactionLogicException(throwables); + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } else { + { + String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } - return null; - }); - } - public static UserInfo getUserInfoUsingId(Start start, AppIdentifier appIdentifier, String id) throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " - + getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ?"; + { + String QUERY = "DELETE FROM " + getConfig(start).getEmailPasswordUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } - UserInfoPartial userInfo = execute(start, QUERY.toString(), pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, id); - }, result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); + { + String QUERY = "DELETE FROM " + getConfig(start).getPasswordResetTokensTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } - return null; - }); - return userInfoWithTenantIds(start, appIdentifier, userInfo); + } } - public static UserInfoPartial getUserInfoUsingId(Start start, Connection sqlCon, AppIdentifier appIdentifier, String id) throws SQLException, StorageQueryException { - // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id table + private static UserInfoPartial getUserInfoUsingId_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + String id) throws SQLException, StorageQueryException { + // we don't need a FOR UPDATE here because this is already part of a transaction, and locked on + // app_id_to_user_id table String QUERY = "SELECT user_id, email, password_hash, time_joined FROM " + getConfig(start).getEmailPasswordUsersTable() + " WHERE app_id = ? AND user_id = ?"; @@ -355,29 +373,55 @@ public static UserInfoPartial getUserInfoUsingId(Start start, Connection sqlCon, }); } - public static List getUsersInfoUsingIdList(Start start, AppIdentifier appIdentifier, List ids) + public static List getUsersInfoUsingIdList(Start start, Set ids, + AppIdentifier appIdentifier) throws SQLException, StorageQueryException { if (ids.size() > 0) { // No need to filter based on tenantId because the id list is already filtered for a tenant - StringBuilder QUERY = new StringBuilder("SELECT user_id, email, password_hash, time_joined " - + "FROM " + getConfig(start).getEmailPasswordUsersTable()); - QUERY.append(" WHERE app_id = ? AND user_id IN ("); - for (int i = 0; i < ids.size(); i++) { - - QUERY.append("?"); - if (i != ids.size() - 1) { - // not the last element - QUERY.append(","); + String QUERY = "SELECT user_id, email, password_hash, time_joined " + + "FROM " + getConfig(start).getEmailPasswordUsersTable() + + " WHERE user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + + " ) AND app_id = ?"; + + List userInfos = execute(start, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; } - } - QUERY.append(")"); + pst.setString(index, appIdentifier.getAppId()); + }, result -> { + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); + } + return finalResult; + }); + fillUserInfoWithTenantIds(start, appIdentifier, userInfos); + fillUserInfoWithVerified(start, appIdentifier, userInfos); + return userInfos.stream().map(UserInfoPartial::toLoginMethod) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } - List userInfos = execute(start, QUERY.toString(), pst -> { - pst.setString(1, appIdentifier.getAppId()); - for (int i = 0; i < ids.size(); i++) { - // i+2 cause this starts with 1 and not 0, and 1 is appId - pst.setString(i + 2, ids.get(i)); + public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids, + AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + if (ids.size() > 0) { + // No need to filter based on tenantId because the id list is already filtered for a tenant + String QUERY = "SELECT user_id, email, password_hash, time_joined " + + "FROM " + getConfig(start).getEmailPasswordUsersTable() + + " WHERE user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + + " ) AND app_id = ?"; + + List userInfos = execute(con, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; } + pst.setString(index, appIdentifier.getAppId()); }, result -> { List finalResult = new ArrayList<>(); while (result.next()) { @@ -385,55 +429,106 @@ public static List getUsersInfoUsingIdList(Start start, AppIdentifier } return finalResult; }); - return userInfoWithTenantIds(start, appIdentifier, userInfos); + fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos); + return userInfos.stream().map(UserInfoPartial::toLoginMethod) + .collect(Collectors.toList()); } return Collections.emptyList(); } + public static String lockEmail_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + String email) + throws StorageQueryException, SQLException { + String QUERY = "SELECT user_id FROM " + getConfig(start).getEmailPasswordUsersTable() + + " WHERE app_id = ? AND email = ?"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + if (result.next()) { + return result.getString("user_id"); + } + return null; + }); + } - public static UserInfo getUserInfoUsingEmail(Start start, TenantIdentifier tenantIdentifier, String email) throws StorageQueryException, SQLException { - String QUERY = "SELECT ep_users_to_tenant.user_id as user_id, ep_users_to_tenant.email as email, " - + "ep_users.password_hash as password_hash, ep_users.time_joined as time_joined " - + "FROM " + getConfig(start).getEmailPasswordUserToTenantTable() + " AS ep_users_to_tenant " - + "JOIN " + getConfig(start).getEmailPasswordUsersTable() + " AS ep_users " - + "ON ep_users.app_id = ep_users_to_tenant.app_id AND ep_users.user_id = ep_users_to_tenant.user_id " - + "WHERE ep_users_to_tenant.app_id = ? AND ep_users_to_tenant.tenant_id = ? AND ep_users_to_tenant.email = ?"; + public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier tenantIdentifier, + String email) + throws StorageQueryException, SQLException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getEmailPasswordUserToTenantTable() + " AS ep" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" + + " WHERE ep.app_id = ? AND ep.tenant_id = ? AND ep.email = ?"; - UserInfoPartial userInfo = execute(start, QUERY, pst -> { + return execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, email); }, result -> { if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); + return result.getString("user_id"); } return null; }); - return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo); + } + + public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String email) + throws StorageQueryException, SQLException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getEmailPasswordUsersTable() + " AS ep" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" + + " ON ep.app_id = all_users.app_id AND ep.user_id = all_users.user_id" + + " WHERE ep.app_id = ? AND ep.email = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); } public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) - throws SQLException, StorageQueryException { - UserInfoPartial userInfo = EmailPasswordQueries.getUserInfoUsingId(start, sqlCon, + throws SQLException, StorageQueryException, UnknownUserIdException { + UserInfoPartial userInfo = EmailPasswordQueries.getUserInfoUsingId_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + if (userInfo == null) { + throw new UnknownUserIdException(); + } + + GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, recipe_id, time_joined)" - + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + GeneralQueries.AccountLinkingInfo finalAccountLinkingInfo = accountLinkingInfo; + update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, userId); - pst.setString(4, EMAIL_PASSWORD.toString()); - pst.setLong(5, userInfo.timeJoined); + pst.setString(4, finalAccountLinkingInfo.primaryUserId); + pst.setBoolean(5, finalAccountLinkingInfo.isLinked); + pst.setString(6, EMAIL_PASSWORD.toString()); + pst.setLong(7, userInfo.timeJoined); + pst.setLong(8, userInfo.timeJoined); }); } { // emailpassword_user_to_tenant String QUERY = "INSERT INTO " + getConfig(start).getEmailPasswordUserToTenantTable() + "(app_id, tenant_id, user_id, email)" - + " VALUES(?, ?, ?, ?) " + " ON CONFLICT DO NOTHING"; + + " VALUES(?, ?, ?, ?) " + " ON CONFLICT (app_id, tenant_id, user_id) DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -446,7 +541,8 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC } } - public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, + TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { { // all_auth_recipe_users String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() @@ -462,42 +558,103 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection // automatically deleted from emailpassword_user_to_tenant because of foreign key constraint } - private static UserInfo userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, Arrays.asList(userInfo)).get(0); - } + return fillUserInfoWithVerified_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0); } - private static List userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithVerified_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userInfos) throws SQLException, StorageQueryException { - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + List userIdsAndEmails = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email)); } + List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified_transaction(start, sqlCon, + appIdentifier, + userIdsAndEmails); + Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified); + for (UserInfoPartial userInfo : userInfos) { + if (verifiedUserIdsSet.contains(userInfo.id)) { + userInfo.verified = true; + } else { + userInfo.verified = false; + } + } + return userInfos; } - private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static List fillUserInfoWithVerified(Start start, + AppIdentifier appIdentifier, + List userInfos) + throws SQLException, StorageQueryException { + List userIdsAndEmails = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email)); + } + List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified(start, + appIdentifier, + userIdsAndEmails); + Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified); + for (UserInfoPartial userInfo : userInfos) { + if (verifiedUserIdsSet.contains(userInfo.id)) { + userInfo.verified = true; + } else { + userInfo.verified = false; + } + } + return userInfos; + } + + private static UserInfoPartial fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - return userInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0); + return fillUserInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0); + } + + private static List fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userInfos) + throws SQLException, StorageQueryException { + String[] userIds = new String[userInfos.size()]; + for (int i = 0; i < userInfos.size(); i++) { + userIds[i] = userInfos.get(i).id; + } + + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, + appIdentifier, + userIds); + List result = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]); + } + + return userInfos; } - private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithTenantIds(Start start, + AppIdentifier appIdentifier, + List userInfos) throws SQLException, StorageQueryException { String[] userIds = new String[userInfos.size()]; for (int i = 0; i < userInfos.size(); i++) { userIds[i] = userInfos.get(i).id; } - Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, appIdentifier, userIds); - List result = new ArrayList<>(); + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds(start, + appIdentifier, + userIds); for (UserInfoPartial userInfo : userInfos) { - result.add(new UserInfo(userInfo.id, userInfo.email, userInfo.passwordHash, userInfo.timeJoined, - tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]))); + userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]); } - return result; + return userInfos; } private static class UserInfoPartial { @@ -505,6 +662,9 @@ private static class UserInfoPartial { public final long timeJoined; public final String email; public final String passwordHash; + public String[] tenantIds; + public Boolean verified; + public Boolean isPrimary; public UserInfoPartial(String id, String email, String passwordHash, long timeJoined) { this.id = id.trim(); @@ -512,6 +672,13 @@ public UserInfoPartial(String id, String email, String passwordHash, long timeJo this.email = email; this.passwordHash = passwordHash; } + + public LoginMethod toLoginMethod() { + assert (tenantIds != null); + assert (verified != null); + return new LoginMethod(id, timeJoined, verified, email, + passwordHash, tenantIds); + } } private static class PasswordResetRowMapper implements RowMapper { @@ -528,7 +695,7 @@ private static PasswordResetRowMapper getInstance() { public PasswordResetTokenInfo map(ResultSet result) throws StorageQueryException { try { return new PasswordResetTokenInfo(result.getString("user_id"), result.getString("token"), - result.getLong("token_expiry")); + result.getLong("token_expiry"), result.getString("email")); } catch (Exception e) { throw new StorageQueryException(e); } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java index 849c8e9fa..c571427ba 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java @@ -17,8 +17,8 @@ package io.supertokens.inmemorydb.queries; import io.supertokens.inmemorydb.ConnectionWithLocks; -import io.supertokens.inmemorydb.QueryExecutorTemplate; import io.supertokens.inmemorydb.Start; +import io.supertokens.inmemorydb.Utils; import io.supertokens.inmemorydb.config.Config; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo; @@ -28,11 +28,9 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute; import static io.supertokens.inmemorydb.QueryExecutorTemplate.update; @@ -80,7 +78,8 @@ public static void deleteExpiredEmailVerificationTokens(Start start) throws SQLE public static void updateUsersIsEmailVerified_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String email, - boolean isEmailVerified) throws SQLException, StorageQueryException { + boolean isEmailVerified) + throws SQLException, StorageQueryException { if (isEmailVerified) { String QUERY = "INSERT INTO " + getConfig(start).getEmailVerificationTable() @@ -104,8 +103,10 @@ public static void updateUsersIsEmailVerified_Transaction(Start start, Connectio } public static void deleteAllEmailVerificationTokensForUser_Transaction(Start start, Connection con, - TenantIdentifier tenantIdentifier, String userId, - String email) throws SQLException, StorageQueryException { + TenantIdentifier tenantIdentifier, + String userId, + String email) + throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?"; @@ -117,7 +118,8 @@ public static void deleteAllEmailVerificationTokensForUser_Transaction(Start sta }); } - public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start start, TenantIdentifier tenantIdentifier, + public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start start, + TenantIdentifier tenantIdentifier, String token) throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry, email FROM " @@ -135,7 +137,8 @@ public static EmailVerificationTokenInfo getEmailVerificationTokenInfo(Start sta }); } - public static void addEmailVerificationToken(Start start, TenantIdentifier tenantIdentifier, String userId, String tokenHash, long expiry, + public static void addEmailVerificationToken(Start start, TenantIdentifier tenantIdentifier, String userId, + String tokenHash, long expiry, String email) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getEmailVerificationTokensTable() + "(app_id, tenant_id, user_id, token, token_expiry, email)" + " VALUES(?, ?, ?, ?, ?, ?)"; @@ -153,12 +156,17 @@ public static void addEmailVerificationToken(Start start, TenantIdentifier tenan public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, - String userId, String email) throws SQLException, StorageQueryException { + String userId, + String email) + throws SQLException, StorageQueryException { - ((ConnectionWithLocks) con).lock(tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + userId + "~" + email + Config.getConfig(start).getEmailVerificationTokensTable()); + ((ConnectionWithLocks) con).lock( + tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + userId + "~" + email + + Config.getConfig(start).getEmailVerificationTokensTable()); String QUERY = "SELECT user_id, token, token_expiry, email FROM " - + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?"; + + getConfig(start).getEmailVerificationTokensTable() + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?"; return execute(con, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -181,9 +189,11 @@ public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUs public static EmailVerificationTokenInfo[] getAllEmailVerificationTokenInfoForUser(Start start, TenantIdentifier tenantIdentifier, String userId, - String email) throws SQLException, StorageQueryException { + String email) + throws SQLException, StorageQueryException { String QUERY = "SELECT user_id, token, token_expiry, email FROM " - + getConfig(start).getEmailVerificationTokensTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?"; + + getConfig(start).getEmailVerificationTokensTable() + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? AND email = ?"; return execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -215,38 +225,130 @@ public static boolean isEmailVerified(Start start, AppIdentifier appIdentifier, }, result -> result.next()); } - public static void deleteUserInfo(Start start, AppIdentifier appIdentifier, String userId) - throws StorageQueryException, StorageTransactionLogicException { - start.startTransaction(con -> { - Connection sqlCon = (Connection) con.getConnection(); - try { - { - String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTable() - + " WHERE app_id = ? AND user_id = ?"; - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }); - } + public static class UserIdAndEmail { + public String userId; + public String email; - { - String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() - + " WHERE app_id = ? AND user_id = ?"; + public UserIdAndEmail(String userId, String email) { + this.userId = userId; + this.email = email; + } + } + + // returns list of userIds where email is verified. + public static List isEmailVerified_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + List userIdAndEmail) + throws SQLException, StorageQueryException { + if (userIdAndEmail.isEmpty()) { + return new ArrayList<>(); + } + List emails = new ArrayList<>(); + List userIds = new ArrayList<>(); + Map userIdToEmailMap = new HashMap<>(); + for (UserIdAndEmail ue : userIdAndEmail) { + emails.add(ue.email); + userIds.add(ue.userId); + } + for (UserIdAndEmail ue : userIdAndEmail) { + if (userIdToEmailMap.containsKey(ue.userId)) { + throw new RuntimeException("Found a bug!"); + } + userIdToEmailMap.put(ue.userId, ue.email); + } + String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable() + + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")"; - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }); + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + int index = 2; + for (String userId : userIds) { + pst.setString(index++, userId); + } + for (String email : emails) { + pst.setString(index++, email); + } + }, result -> { + List res = new ArrayList<>(); + while (result.next()) { + String userId = result.getString("user_id"); + String email = result.getString("email"); + if (Objects.equals(userIdToEmailMap.get(userId), email)) { + res.add(userId); } + } + return res; + }); + } - sqlCon.commit(); - } catch (SQLException throwables) { - throw new StorageTransactionLogicException(throwables); + public static List isEmailVerified(Start start, AppIdentifier appIdentifier, + List userIdAndEmail) + throws SQLException, StorageQueryException { + if (userIdAndEmail.isEmpty()) { + return new ArrayList<>(); + } + List emails = new ArrayList<>(); + List userIds = new ArrayList<>(); + Map userIdToEmailMap = new HashMap<>(); + for (UserIdAndEmail ue : userIdAndEmail) { + emails.add(ue.email); + userIds.add(ue.userId); + } + for (UserIdAndEmail ue : userIdAndEmail) { + if (userIdToEmailMap.containsKey(ue.userId)) { + throw new RuntimeException("Found a bug!"); } - return null; + userIdToEmailMap.put(ue.userId, ue.email); + } + String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable() + + " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + int index = 2; + for (String userId : userIds) { + pst.setString(index++, userId); + } + for (String email : emails) { + pst.setString(index++, email); + } + }, result -> { + List res = new ArrayList<>(); + while (result.next()) { + String userId = result.getString("user_id"); + String email = result.getString("email"); + if (Objects.equals(userIdToEmailMap.get(userId), email)) { + res.add(userId); + } + } + return res; }); } + public static void deleteUserInfo_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId) + throws StorageQueryException, SQLException { + { + String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + + { + String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() + + " WHERE app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + } + public static boolean deleteUserInfo(Start start, TenantIdentifier tenantIdentifier, String userId) throws StorageQueryException, SQLException { String QUERY = "DELETE FROM " + getConfig(start).getEmailVerificationTokensTable() diff --git a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java index dc56ccce4..96ccc6773 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java @@ -20,15 +20,16 @@ import io.supertokens.inmemorydb.ConnectionPool; import io.supertokens.inmemorydb.ConnectionWithLocks; import io.supertokens.inmemorydb.Start; +import io.supertokens.inmemorydb.Utils; import io.supertokens.inmemorydb.config.Config; import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.RowMapper; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; -import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -38,10 +39,8 @@ import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; import static io.supertokens.ProcessState.PROCESS_STATE.CREATING_NEW_TABLE; import static io.supertokens.ProcessState.getInstance; @@ -74,19 +73,69 @@ static String getQueryToCreateUsersTable(Start start) { + "app_id VARCHAR(64) DEFAULT 'public'," + "tenant_id VARCHAR(64) DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," + + "primary_or_recipe_user_id CHAR(36) NOT NULL," + + "is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE," + + "primary_or_recipe_user_time_joined BIGINT UNSIGNED NOT NULL," + "recipe_id VARCHAR(128) NOT NULL," + "time_joined BIGINT UNSIGNED NOT NULL," + "PRIMARY KEY (app_id, tenant_id, user_id)," + "FOREIGN KEY (app_id, tenant_id) REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE," + + "FOREIGN KEY (app_id, primary_or_recipe_user_id) REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + + " (app_id, user_id) ON DELETE CASCADE," + "FOREIGN KEY (app_id, user_id) REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + " (app_id, user_id) ON DELETE CASCADE" + ");"; } - static String getQueryToCreateUserPaginationIndex(Start start) { - return "CREATE INDEX all_auth_recipe_users_pagination_index ON " + Config.getConfig(start).getUsersTable() - + "(time_joined DESC, user_id DESC, tenant_id DESC, app_id DESC);"; + public static String getQueryToCreateUserIdIndexForUsersTable(Start start) { + return "CREATE INDEX IF NOT EXISTS all_auth_recipe_user_id_index ON " + + Config.getConfig(start).getUsersTable() + "(app_id, user_id);"; + } + + public static String getQueryToCreateTenantIdIndexForUsersTable(Start start) { + return "CREATE INDEX IF NOT EXISTS all_auth_recipe_tenant_id_index ON " + + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id);"; + } + + static String getQueryToCreateUserPaginationIndex1(Start start) { + return "CREATE INDEX all_auth_recipe_users_pagination_index1 ON " + Config.getConfig(start).getUsersTable() + + "(app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);"; + } + + static String getQueryToCreateUserPaginationIndex2(Start start) { + return "CREATE INDEX all_auth_recipe_users_pagination_index2 ON " + Config.getConfig(start).getUsersTable() + + "(app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC);"; + } + + static String getQueryToCreateUserPaginationIndex3(Start start) { + return "CREATE INDEX all_auth_recipe_users_pagination_index3 ON " + Config.getConfig(start).getUsersTable() + + "(recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);"; + } + + static String getQueryToCreateUserPaginationIndex4(Start start) { + return "CREATE INDEX all_auth_recipe_users_pagination_index4 ON " + Config.getConfig(start).getUsersTable() + + "(recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC);"; + } + + static String getQueryToCreatePrimaryUserId(Start start) { + /* + * Used in: + * - does user exist + * + * */ + return "CREATE INDEX all_auth_recipe_users_primary_user_id_index ON " + Config.getConfig(start).getUsersTable() + + "(app_id, primary_or_recipe_user_id);"; + } + + static String getQueryToCreateRecipeIdIndex(Start start) { + /* + * Used in: + * - user count query + * */ + return "CREATE INDEX all_auth_recipe_users_recipe_id_index ON " + + Config.getConfig(start).getUsersTable() + + "(app_id, recipe_id, tenant_id);"; } private static String getQueryToCreateAppsTable(Start start) { @@ -128,15 +177,23 @@ static String getQueryToCreateKeyValueTable(Start start) { } + static String getQueryToCreateTenantIdIndexForKeyValueTable(Start start) { + return "CREATE INDEX IF NOT EXISTS key_value_tenant_id_index ON " + + Config.getConfig(start).getKeyValueTable() + "(app_id, tenant_id);"; + } - private static String getQueryToCreateAppIdToUserIdTable(Start start) { + private static String getQueryToCreateAppIdToUserIdTable(Start start) { String appToUserTable = Config.getConfig(start).getAppIdToUserIdTable(); // @formatter:off return "CREATE TABLE IF NOT EXISTS " + appToUserTable + " (" + "app_id VARCHAR(64) NOT NULL DEFAULT 'public'," + "user_id CHAR(36) NOT NULL," + "recipe_id VARCHAR(128) NOT NULL," + + "primary_or_recipe_user_id CHAR(36) NOT NULL," + + "is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE," + "PRIMARY KEY (app_id, user_id), " + + "FOREIGN KEY (app_id, primary_or_recipe_user_id) REFERENCES " + Config.getConfig(start).getAppIdToUserIdTable() + + " (app_id, user_id) ON DELETE CASCADE," + "FOREIGN KEY(app_id) REFERENCES " + Config.getConfig(start).getAppsTable() + " (app_id) ON DELETE CASCADE" + ");"; @@ -157,6 +214,8 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc if (!doesTableExists(start, Config.getConfig(start).getKeyValueTable())) { getInstance(main).addState(CREATING_NEW_TABLE, null); update(start, getQueryToCreateKeyValueTable(start), NO_OP_SETTER); + // index + update(start, getQueryToCreateTenantIdIndexForKeyValueTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getAppIdToUserIdTable())) { @@ -169,7 +228,12 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc update(start, getQueryToCreateUsersTable(start), NO_OP_SETTER); // index - update(start, getQueryToCreateUserPaginationIndex(start), NO_OP_SETTER); + update(start, getQueryToCreateUserPaginationIndex1(start), NO_OP_SETTER); + update(start, getQueryToCreateUserPaginationIndex2(start), NO_OP_SETTER); + update(start, getQueryToCreateUserPaginationIndex3(start), NO_OP_SETTER); + update(start, getQueryToCreateUserPaginationIndex4(start), NO_OP_SETTER); + update(start, getQueryToCreatePrimaryUserId(start), NO_OP_SETTER); + update(start, getQueryToCreateRecipeIdIndex(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUserLastActiveTable())) { @@ -199,6 +263,7 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc getInstance(main).addState(CREATING_NEW_TABLE, null); update(start, MultitenancyQueries.getQueryToCreateTenantThirdPartyProvidersTable(start), NO_OP_SETTER); + } if (!doesTableExists(start, Config.getConfig(start).getTenantThirdPartyProviderClientsTable())) { @@ -280,7 +345,6 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc // index update(start, getQueryToCreateCodeCreatedAtIndex(start), NO_OP_SETTER); update(start, getQueryToCreateCodeDeviceIdHashIndex(start), NO_OP_SETTER); - } if (!doesTableExists(start, Config.getConfig(start).getUserMetadataTable())) { @@ -290,22 +354,22 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc if (!doesTableExists(start, Config.getConfig(start).getRolesTable())) { getInstance(main).addState(CREATING_NEW_TABLE, null); - update(start, UserRoleQueries.getQueryToCreateRolesTable(start), NO_OP_SETTER); + update(start, UserRolesQueries.getQueryToCreateRolesTable(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUserRolesPermissionsTable())) { getInstance(main).addState(CREATING_NEW_TABLE, null); - update(start, UserRoleQueries.getQueryToCreateRolePermissionsTable(start), NO_OP_SETTER); + update(start, UserRolesQueries.getQueryToCreateRolePermissionsTable(start), NO_OP_SETTER); // index - update(start, UserRoleQueries.getQueryToCreateRolePermissionsPermissionIndex(start), NO_OP_SETTER); + update(start, UserRolesQueries.getQueryToCreateRolePermissionsPermissionIndex(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUserRolesTable())) { getInstance(main).addState(CREATING_NEW_TABLE, null); - update(start, UserRoleQueries.getQueryToCreateUserRolesTable(start), NO_OP_SETTER); + update(start, UserRolesQueries.getQueryToCreateUserRolesTable(start), NO_OP_SETTER); // index - update(start, UserRoleQueries.getQueryToCreateUserRolesRoleIndex(start), NO_OP_SETTER); + update(start, UserRolesQueries.getQueryToCreateUserRolesRoleIndex(start), NO_OP_SETTER); } if (!doesTableExists(start, Config.getConfig(start).getUserIdMappingTable())) { @@ -392,7 +456,9 @@ public static KeyValueInfo getKeyValue_Transaction(Start start, Connection con, String key) throws SQLException, StorageQueryException { - ((ConnectionWithLocks) con).lock(tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + key + Config.getConfig(start).getKeyValueTable()); + ((ConnectionWithLocks) con).lock( + tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + key + + Config.getConfig(start).getKeyValueTable()); String QUERY = "SELECT value, created_at_time FROM " + getConfig(start).getKeyValueTable() + " WHERE app_id = ? AND tenant_id = ? AND name = ?"; @@ -424,7 +490,9 @@ public static void deleteKeyValue_Transaction(Start start, Connection con, Tenan public static long getUsersCount(Start start, AppIdentifier appIdentifier, RECIPE_ID[] includeRecipeIds) throws SQLException, StorageQueryException { - StringBuilder QUERY = new StringBuilder("SELECT COUNT(*) as total FROM " + getConfig(start).getUsersTable()); + StringBuilder QUERY = new StringBuilder( + "SELECT COUNT(DISTINCT primary_or_recipe_user_id) AS total FROM " + + getConfig(start).getUsersTable()); QUERY.append(" WHERE app_id = ?"); if (includeRecipeIds != null && includeRecipeIds.length > 0) { QUERY.append(" AND recipe_id IN ("); @@ -456,7 +524,8 @@ public static long getUsersCount(Start start, AppIdentifier appIdentifier, RECIP public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier, RECIPE_ID[] includeRecipeIds) throws SQLException, StorageQueryException { - StringBuilder QUERY = new StringBuilder("SELECT COUNT(*) as total FROM " + getConfig(start).getUsersTable()); + StringBuilder QUERY = new StringBuilder( + "SELECT COUNT(DISTINCT primary_or_recipe_user_id) AS total FROM " + getConfig(start).getUsersTable()); QUERY.append(" WHERE app_id = ? AND tenant_id = ?"); if (includeRecipeIds != null && includeRecipeIds.length > 0) { QUERY.append(" AND recipe_id IN ("); @@ -489,37 +558,46 @@ public static long getUsersCount(Start start, TenantIdentifier tenantIdentifier, public static boolean doesUserIdExist(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - + // We query both tables cause there is a case where a primary user ID exists, but its associated + // recipe user ID has been deleted AND there are other recipe user IDs linked to this primary user ID already. String QUERY = "SELECT 1 FROM " + getConfig(start).getAppIdToUserIdTable() - + " WHERE app_id = ? AND user_id = ?"; + + " WHERE app_id = ? AND user_id = ? UNION SELECT 1 FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND primary_or_recipe_user_id = ?"; return execute(start, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); + pst.setString(3, appIdentifier.getAppId()); + pst.setString(4, userId); }, ResultSet::next); } public static boolean doesUserIdExist(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { - + // We query both tables cause there is a case where a primary user ID exists, but its associated + // recipe user ID has been deleted AND there are other recipe user IDs linked to this primary user ID already. String QUERY = "SELECT 1 FROM " + getConfig(start).getUsersTable() - + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; + + " WHERE app_id = ? AND tenant_id = ? AND user_id = ? UNION SELECT 1 FROM " + + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND tenant_id = ? AND primary_or_recipe_user_id = ?"; return execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, userId); + pst.setString(4, tenantIdentifier.getAppId()); + pst.setString(5, tenantIdentifier.getTenantId()); + pst.setString(6, userId); }, ResultSet::next); } - public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenantIdentifier, @NotNull Integer limit, - @NotNull String timeJoinedOrder, - @org.jetbrains.annotations.Nullable RECIPE_ID[] includeRecipeIds, @org.jetbrains.annotations.Nullable - String userId, - @org.jetbrains.annotations.Nullable Long timeJoined, - @Nullable DashboardSearchTags dashboardSearchTags) + public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenantIdentifier, @NotNull Integer limit, + @NotNull String timeJoinedOrder, + @Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, + @Nullable Long timeJoined, + @Nullable DashboardSearchTags dashboardSearchTags) throws SQLException, StorageQueryException { // This list will be used to keep track of the result's order from the db - List usersFromQuery; + List usersFromQuery; if (dashboardSearchTags != null) { ArrayList queryList = new ArrayList<>(); @@ -561,7 +639,9 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant String QUERY = "SELECT allAuthUsersTable.*" + " FROM " + getConfig(start).getUsersTable() + " AS allAuthUsersTable" + " JOIN " + getConfig(start).getThirdPartyUserToTenantTable() - + " AS thirdPartyToTenantTable ON allAuthUsersTable.app_id = thirdPartyToTenantTable.app_id AND" + + + " AS thirdPartyToTenantTable ON allAuthUsersTable.app_id = thirdPartyToTenantTable" + + ".app_id AND" + " allAuthUsersTable.tenant_id = thirdPartyToTenantTable.tenant_id AND" + " allAuthUsersTable.user_id = thirdPartyToTenantTable.user_id" + " JOIN " + getConfig(start).getThirdPartyUsersTable() @@ -691,22 +771,20 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant usersFromQuery = new ArrayList<>(); } else { - String finalQuery = "SELECT * FROM ( " + USER_SEARCH_TAG_CONDITION.toString() + " )" - + " AS finalResultTable ORDER BY time_joined " + timeJoinedOrder + ", user_id DESC "; + String finalQuery = "SELECT DISTINCT primary_or_recipe_user_id, primary_or_recipe_user_time_joined FROM ( " + USER_SEARCH_TAG_CONDITION.toString() + " )" + + " AS finalResultTable ORDER BY primary_or_recipe_user_time_joined " + timeJoinedOrder + ", primary_or_recipe_user_id DESC "; usersFromQuery = execute(start, finalQuery, pst -> { for (int i = 1; i <= queryList.size(); i++) { pst.setString(i, queryList.get(i - 1)); } }, result -> { - List temp = new ArrayList<>(); + List temp = new ArrayList<>(); while (result.next()) { - temp.add(new UserInfoPaginationResultHolder(result.getString("user_id"), - result.getString("recipe_id"))); + temp.add(result.getString("primary_or_recipe_user_id")); } return temp; }); } - } } else { @@ -730,11 +808,11 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant recipeIdCondition = recipeIdCondition + " AND"; } String timeJoinedOrderSymbol = timeJoinedOrder.equals("ASC") ? ">" : "<"; - String QUERY = "SELECT user_id, recipe_id FROM " + getConfig(start).getUsersTable() + " WHERE " - + recipeIdCondition + " (time_joined " + timeJoinedOrderSymbol - + " ? OR (time_joined = ? AND user_id <= ?)) AND app_id = ? AND tenant_id = ?" - + " ORDER BY time_joined " + timeJoinedOrder - + ", user_id DESC LIMIT ?"; + String QUERY = "SELECT DISTINCT primary_or_recipe_user_id, primary_or_recipe_user_time_joined FROM " + getConfig(start).getUsersTable() + " WHERE " + + recipeIdCondition + " (primary_or_recipe_user_time_joined " + timeJoinedOrderSymbol + + " ? OR (primary_or_recipe_user_time_joined = ? AND primary_or_recipe_user_id <= ?)) AND app_id = ? AND tenant_id = ?" + + " ORDER BY primary_or_recipe_user_time_joined " + timeJoinedOrder + + ", primary_or_recipe_user_id DESC LIMIT ?"; usersFromQuery = execute(start, QUERY, pst -> { if (includeRecipeIds != null) { for (int i = 0; i < includeRecipeIds.length; i++) { @@ -750,21 +828,20 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant pst.setString(baseIndex + 5, tenantIdentifier.getTenantId()); pst.setInt(baseIndex + 6, limit); }, result -> { - List temp = new ArrayList<>(); + List temp = new ArrayList<>(); while (result.next()) { - temp.add(new UserInfoPaginationResultHolder(result.getString("user_id"), - result.getString("recipe_id"))); + temp.add(result.getString("primary_or_recipe_user_id")); } return temp; }); } else { String recipeIdCondition = RECIPE_ID_CONDITION.toString(); - String QUERY = "SELECT user_id, recipe_id FROM " + getConfig(start).getUsersTable() + " WHERE "; + String QUERY = "SELECT DISTINCT primary_or_recipe_user_id, primary_or_recipe_user_time_joined FROM " + getConfig(start).getUsersTable() + " WHERE "; if (!recipeIdCondition.equals("")) { QUERY += recipeIdCondition + " AND"; } - QUERY += " app_id = ? AND tenant_id = ? ORDER BY time_joined " + timeJoinedOrder - + ", user_id DESC LIMIT ?"; + QUERY += " app_id = ? AND tenant_id = ? ORDER BY primary_or_recipe_user_time_joined " + timeJoinedOrder + + ", primary_or_recipe_user_id DESC LIMIT ?"; usersFromQuery = execute(start, QUERY, pst -> { if (includeRecipeIds != null) { for (int i = 0; i < includeRecipeIds.length; i++) { @@ -777,78 +854,528 @@ public static AuthRecipeUserInfo[] getUsers(Start start, TenantIdentifier tenant pst.setString(baseIndex + 2, tenantIdentifier.getTenantId()); pst.setInt(baseIndex + 3, limit); }, result -> { - List temp = new ArrayList<>(); + List temp = new ArrayList<>(); while (result.next()) { - temp.add(new UserInfoPaginationResultHolder(result.getString("user_id"), - result.getString("recipe_id"))); + temp.add(result.getString("primary_or_recipe_user_id")); } return temp; }); } } - // we create a map from recipe ID -> userId[] - Map> recipeIdToUserIdListMap = new HashMap<>(); - for (UserInfoPaginationResultHolder user : usersFromQuery) { - RECIPE_ID recipeId = RECIPE_ID.getEnumFromString(user.recipeId); - if (recipeId == null) { - throw new SQLException("Unrecognised recipe ID in database: " + user.recipeId); + AuthRecipeUserInfo[] finalResult = new AuthRecipeUserInfo[usersFromQuery.size()]; + + List users = getPrimaryUserInfoForUserIds(start, + tenantIdentifier.toAppIdentifier(), + usersFromQuery); + + // we fill in all the slots in finalResult based on their position in + // usersFromQuery + Map userIdToInfoMap = new HashMap<>(); + for (AuthRecipeUserInfo user : users) { + userIdToInfoMap.put(user.getSupertokensUserId(), user); + } + for (int i = 0; i < usersFromQuery.size(); i++) { + if (finalResult[i] == null) { + finalResult[i] = userIdToInfoMap.get(usersFromQuery.get(i)); } - List userIdList = recipeIdToUserIdListMap.get(recipeId); - if (userIdList == null) { - userIdList = new ArrayList<>(); + } + + return finalResult; + } + + public static void makePrimaryUser_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + String userId) + throws SQLException, StorageQueryException { + { + String QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET is_linked_or_is_a_primary_user = true WHERE app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + { + String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() + + " SET is_linked_or_is_a_primary_user = true WHERE app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + } + + public static void linkAccounts_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + String recipeUserId, String primaryUserId) + throws SQLException, StorageQueryException { + { + String QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = ? WHERE app_id = ? AND " + + "user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, primaryUserId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, recipeUserId); + }); + } + { // update primary_or_recipe_user_time_joined to min time joined + String QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET primary_or_recipe_user_time_joined = (SELECT MIN(time_joined) FROM " + + getConfig(start).getUsersTable() + " WHERE app_id = ? AND primary_or_recipe_user_id = ?) WHERE " + + " app_id = ? AND primary_or_recipe_user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, primaryUserId); + pst.setString(3, appIdentifier.getAppId()); + pst.setString(4, primaryUserId); + }); + } + { + String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() + + " SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = ? WHERE app_id = ? AND " + + "user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, primaryUserId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, recipeUserId); + }); + } + } + + public static void unlinkAccounts_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + String primaryUserId, String recipeUserId) + throws SQLException, StorageQueryException { + { + String QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET is_linked_or_is_a_primary_user = false, primary_or_recipe_user_id = ?, " + + "primary_or_recipe_user_time_joined = time_joined WHERE app_id = ? AND " + + "user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, recipeUserId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, recipeUserId); + }); + } + { // update primary_or_recipe_user_time_joined to min time joined + String QUERY = "UPDATE " + getConfig(start).getUsersTable() + + " SET primary_or_recipe_user_time_joined = (SELECT MIN(time_joined) FROM " + + getConfig(start).getUsersTable() + " WHERE app_id = ? AND primary_or_recipe_user_id = ?) WHERE " + + " app_id = ? AND primary_or_recipe_user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, primaryUserId); + pst.setString(3, appIdentifier.getAppId()); + pst.setString(4, primaryUserId); + }); + } + { + String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() + + " SET is_linked_or_is_a_primary_user = false, primary_or_recipe_user_id = ?" + + " WHERE app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY, pst -> { + pst.setString(1, recipeUserId); + pst.setString(2, appIdentifier.getAppId()); + pst.setString(3, recipeUserId); + }); + } + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + String phoneNumber) + throws SQLException, StorageQueryException { + // we first lock on the table based on phoneNumber and tenant - this will ensure that any other + // query happening related to the account linking on this phone number / tenant will wait for this to finish, + // and vice versa. + + PasswordlessQueries.lockPhone_Transaction(start, sqlCon, appIdentifier, phoneNumber); + + // now that we have locks on all the relevant tables, we can read from them safely + List userIds = PasswordlessQueries.listUserIdsByPhoneNumber_Transaction(start, sqlCon, appIdentifier, + phoneNumber); + + List result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier, + userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo(Start start, + AppIdentifier appIdentifier, + String thirdPartyId, + String thirdPartyUserId) + throws SQLException, StorageQueryException { + List userIds = ThirdPartyQueries.listUserIdsByThirdPartyInfo(start, appIdentifier, + thirdPartyId, thirdPartyUserId); + List result = getPrimaryUserInfoForUserIds(start, appIdentifier, userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + String thirdPartyId, + String thirdPartyUserId) + throws SQLException, StorageQueryException { + // we first lock on the table based on thirdparty info and tenant - this will ensure that any other + // query happening related to the account linking on this third party info / tenant will wait for this to + // finish, + // and vice versa. + + ThirdPartyQueries.lockThirdPartyInfo_Transaction(start, sqlCon, appIdentifier, thirdPartyId, + thirdPartyUserId); + + // now that we have locks on all the relevant tables, we can read from them safely + List userIds = ThirdPartyQueries.listUserIdsByThirdPartyInfo_Transaction(start, sqlCon, appIdentifier, + thirdPartyId, thirdPartyUserId); + List result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier, userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + String email) + throws SQLException, StorageQueryException { + // we first lock on the three tables based on email and tenant - this will ensure that any other + // query happening related to the account linking on this email / tenant will wait for this to finish, + // and vice versa. + + EmailPasswordQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email); + + ThirdPartyQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email); + + PasswordlessQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email); + + // now that we have locks on all the relevant tables, we can read from them safely + List userIds = new ArrayList<>(); + userIds.addAll(EmailPasswordQueries.getPrimaryUserIdsUsingEmail_Transaction(start, sqlCon, appIdentifier, + email)); + + userIds.addAll(PasswordlessQueries.getPrimaryUserIdsUsingEmail_Transaction(start, sqlCon, appIdentifier, + email)); + + userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail_Transaction(start, sqlCon, appIdentifier, email)); + + // remove duplicates from userIds + Set userIdsSet = new HashSet<>(userIds); + userIds = new ArrayList<>(userIdsSet); + + List result = getPrimaryUserInfoForUserIds(start, appIdentifier, + userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByEmail(Start start, TenantIdentifier tenantIdentifier, + String email) + throws StorageQueryException, SQLException { + List userIds = new ArrayList<>(); + String emailPasswordUserId = EmailPasswordQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier, + email); + if (emailPasswordUserId != null) { + userIds.add(emailPasswordUserId); + } + + String passwordlessUserId = PasswordlessQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier, + email); + if (passwordlessUserId != null) { + userIds.add(passwordlessUserId); + } + + userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail(start, tenantIdentifier, email)); + + // remove duplicates from userIds + Set userIdsSet = new HashSet<>(userIds); + userIds = new ArrayList<>(userIdsSet); + + List result = getPrimaryUserInfoForUserIds(start, tenantIdentifier.toAppIdentifier(), + userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber(Start start, + TenantIdentifier tenantIdentifier, + String phoneNumber) + throws StorageQueryException, SQLException { + List userIds = new ArrayList<>(); + + String passwordlessUserId = PasswordlessQueries.getPrimaryUserByPhoneNumber(start, tenantIdentifier, + phoneNumber); + if (passwordlessUserId != null) { + userIds.add(passwordlessUserId); + } + + List result = getPrimaryUserInfoForUserIds(start, tenantIdentifier.toAppIdentifier(), + userIds); + + // this is going to order them based on oldest that joined to newest that joined. + result.sort(Comparator.comparingLong(o -> o.timeJoined)); + + return result.toArray(new AuthRecipeUserInfo[0]); + } + + public static AuthRecipeUserInfo getPrimaryUserByThirdPartyInfo(Start start, + TenantIdentifier tenantIdentifier, + String thirdPartyId, + String thirdPartyUserId) + throws StorageQueryException, SQLException { + String userId = ThirdPartyQueries.getUserIdByThirdPartyInfo(start, tenantIdentifier, + thirdPartyId, thirdPartyUserId); + return getPrimaryUserInfoForUserId(start, tenantIdentifier.toAppIdentifier(), userId); + } + + public static String getPrimaryUserIdStrForUserId(Start start, AppIdentifier appIdentifier, String id) + throws SQLException, StorageQueryException { + String QUERY = "SELECT primary_or_recipe_user_id FROM " + getConfig(start).getUsersTable() + + " WHERE user_id = ? AND app_id = ?"; + return execute(start, QUERY, pst -> { + pst.setString(1, id); + pst.setString(2, appIdentifier.getAppId()); + }, result -> { + if (result.next()) { + return result.getString("primary_or_recipe_user_id"); + } + return null; + }); + } + + public static AuthRecipeUserInfo getPrimaryUserInfoForUserId(Start start, AppIdentifier appIdentifier, String id) + throws SQLException, StorageQueryException { + List ids = new ArrayList<>(); + ids.add(id); + List result = getPrimaryUserInfoForUserIds(start, appIdentifier, ids); + if (result.isEmpty()) { + return null; + } + return result.get(0); + } + + public static AuthRecipeUserInfo getPrimaryUserInfoForUserId_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String id) + throws SQLException, StorageQueryException { + List ids = new ArrayList<>(); + ids.add(id); + List result = getPrimaryUserInfoForUserIds_Transaction(start, con, appIdentifier, ids); + if (result.isEmpty()) { + return null; + } + return result.get(0); + } + + private static List getPrimaryUserInfoForUserIds(Start start, + AppIdentifier appIdentifier, + List userIds) + throws StorageQueryException, SQLException { + if (userIds.size() == 0) { + return new ArrayList<>(); + } + + // We check both user_id and primary_or_recipe_user_id because the input may have a recipe userId + // which is linked to a primary user ID in which case it won't be in the primary_or_recipe_user_id column, + // or the input may have a primary user ID whose recipe user ID was removed, so it won't be in the user_id + // column + String QUERY = "SELECT au.user_id, au.primary_or_recipe_user_id, au.is_linked_or_is_a_primary_user, au.recipe_id, aaru.tenant_id, aaru.time_joined FROM " + getConfig(start).getAppIdToUserIdTable() + " as au " + + "LEFT JOIN " + getConfig(start).getUsersTable() + " as aaru ON au.app_id = aaru.app_id AND au.user_id = aaru.user_id" + + " WHERE au.primary_or_recipe_user_id IN (SELECT primary_or_recipe_user_id FROM " + + getConfig(start).getAppIdToUserIdTable() + " WHERE (user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ") OR au.primary_or_recipe_user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ")) AND app_id = ?) AND au.app_id = ?"; + + List allAuthUsersResult = execute(start, QUERY, pst -> { + // IN user_id + int index = 1; + for (int i = 0; i < userIds.size(); i++, index++) { + pst.setString(index, userIds.get(i)); + } + // IN primary_or_recipe_user_id + for (int i = 0; i < userIds.size(); i++, index++) { + pst.setString(index, userIds.get(i)); + } + // for app_id + pst.setString(index, appIdentifier.getAppId()); + pst.setString(index + 1, appIdentifier.getAppId()); + }, result -> { + List parsedResult = new ArrayList<>(); + while (result.next()) { + parsedResult.add(new AllAuthRecipeUsersResultHolder(result.getString("user_id"), + result.getString("tenant_id"), + result.getString("primary_or_recipe_user_id"), + result.getBoolean("is_linked_or_is_a_primary_user"), + result.getString("recipe_id"), + result.getLong("time_joined"))); } - userIdList.add(user.userId); - recipeIdToUserIdListMap.put(recipeId, userIdList); + return parsedResult; + }); + + // Now we form the userIds again, but based on the user_id in the result from above. + Set recipeUserIdsToFetch = new HashSet<>(); + for (AllAuthRecipeUsersResultHolder user : allAuthUsersResult) { + // this will remove duplicate entries wherein a user id is shared across several tenants. + recipeUserIdsToFetch.add(user.userId); } - AuthRecipeUserInfo[] finalResult = new AuthRecipeUserInfo[usersFromQuery.size()]; + List loginMethods = new ArrayList<>(); + loginMethods.addAll( + EmailPasswordQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier)); + loginMethods.addAll(ThirdPartyQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier)); + loginMethods.addAll( + PasswordlessQueries.getUsersInfoUsingIdList(start, recipeUserIdsToFetch, appIdentifier)); + + Map recipeUserIdToLoginMethodMap = new HashMap<>(); + for (LoginMethod loginMethod : loginMethods) { + recipeUserIdToLoginMethodMap.put(loginMethod.getSupertokensUserId(), loginMethod); + } - // we give the userId[] for each recipe to fetch all those user's details - for (RECIPE_ID recipeId : recipeIdToUserIdListMap.keySet()) { - List users = getUserInfoForRecipeIdFromUserIds(start, - tenantIdentifier.toAppIdentifier(), recipeId, recipeIdToUserIdListMap.get(recipeId)); + Map userIdToAuthRecipeUserInfo = new HashMap<>(); + + for (AllAuthRecipeUsersResultHolder authRecipeUsersResultHolder : allAuthUsersResult) { + String recipeUserId = authRecipeUsersResultHolder.userId; + LoginMethod loginMethod = recipeUserIdToLoginMethodMap.get(recipeUserId); - // we fill in all the slots in finalResult based on their position in - // usersFromQuery - Map userIdToInfoMap = new HashMap<>(); - for (AuthRecipeUserInfo user : users) { - userIdToInfoMap.put(user.id, user); + if (loginMethod == null) { + // loginMethod will be null for primaryUserId for which the user has been deleted during unlink + continue; } - for (int i = 0; i < usersFromQuery.size(); i++) { - if (finalResult[i] == null) { - finalResult[i] = userIdToInfoMap.get(usersFromQuery.get(i).userId); - } + + String primaryUserId = authRecipeUsersResultHolder.primaryOrRecipeUserId; + AuthRecipeUserInfo curr = userIdToAuthRecipeUserInfo.get(primaryUserId); + if (curr == null) { + curr = AuthRecipeUserInfo.create(primaryUserId, authRecipeUsersResultHolder.isLinkedOrIsAPrimaryUser, + loginMethod); + } else { + curr.addLoginMethod(loginMethod); } + userIdToAuthRecipeUserInfo.put(primaryUserId, curr); } - return finalResult; + return userIdToAuthRecipeUserInfo.keySet().stream().map(userIdToAuthRecipeUserInfo::get) + .collect(Collectors.toList()); } - private static List getUserInfoForRecipeIdFromUserIds(Start start, - AppIdentifier appIdentifier, - RECIPE_ID recipeId, - List userIds) + private static List getPrimaryUserInfoForUserIds_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userIds) throws StorageQueryException, SQLException { - if (recipeId == RECIPE_ID.EMAIL_PASSWORD) { - return EmailPasswordQueries.getUsersInfoUsingIdList(start, appIdentifier, userIds); - } else if (recipeId == RECIPE_ID.THIRD_PARTY) { - return ThirdPartyQueries.getUsersInfoUsingIdList(start, appIdentifier, userIds); - } else if (recipeId == RECIPE_ID.PASSWORDLESS) { - return PasswordlessQueries.getUsersByIdList(start, appIdentifier, userIds); - } else { - throw new IllegalArgumentException("No implementation of get users for recipe: " + recipeId.toString()); + if (userIds.size() == 0) { + return new ArrayList<>(); + } + + // We check both user_id and primary_or_recipe_user_id because the input may have a recipe userId + // which is linked to a primary user ID in which case it won't be in the primary_or_recipe_user_id column, + // or the input may have a primary user ID whose recipe user ID was removed, so it won't be in the user_id + // column + String QUERY = "SELECT au.user_id, au.primary_or_recipe_user_id, au.is_linked_or_is_a_primary_user, au.recipe_id, aaru.tenant_id, aaru.time_joined FROM " + getConfig(start).getAppIdToUserIdTable() + " as au" + + " LEFT JOIN " + getConfig(start).getUsersTable() + " as aaru ON au.app_id = aaru.app_id AND au.user_id = aaru.user_id" + + " WHERE au.primary_or_recipe_user_id IN (SELECT primary_or_recipe_user_id FROM " + + getConfig(start).getAppIdToUserIdTable() + " WHERE (user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ") OR au.primary_or_recipe_user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) + + ")) AND app_id = ?) AND au.app_id = ?"; + + List allAuthUsersResult = execute(sqlCon, QUERY, pst -> { + // IN user_id + int index = 1; + for (int i = 0; i < userIds.size(); i++, index++) { + pst.setString(index, userIds.get(i)); + } + // IN primary_or_recipe_user_id + for (int i = 0; i < userIds.size(); i++, index++) { + pst.setString(index, userIds.get(i)); + } + // for app_id + pst.setString(index, appIdentifier.getAppId()); + pst.setString(index + 1, appIdentifier.getAppId()); + }, result -> { + List parsedResult = new ArrayList<>(); + while (result.next()) { + parsedResult.add(new AllAuthRecipeUsersResultHolder(result.getString("user_id"), + result.getString("tenant_id"), + result.getString("primary_or_recipe_user_id"), + result.getBoolean("is_linked_or_is_a_primary_user"), + result.getString("recipe_id"), + result.getLong("time_joined"))); + } + return parsedResult; + }); + + // Now we form the userIds again, but based on the user_id in the result from above. + Set recipeUserIdsToFetch = new HashSet<>(); + for (AllAuthRecipeUsersResultHolder user : allAuthUsersResult) { + // this will remove duplicate entries wherein a user id is shared across several tenants. + recipeUserIdsToFetch.add(user.userId); + } + + List loginMethods = new ArrayList<>(); + loginMethods.addAll( + EmailPasswordQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier)); + loginMethods.addAll(ThirdPartyQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier)); + loginMethods.addAll( + PasswordlessQueries.getUsersInfoUsingIdList_Transaction(start, sqlCon, recipeUserIdsToFetch, appIdentifier)); + + Map recipeUserIdToLoginMethodMap = new HashMap<>(); + for (LoginMethod loginMethod : loginMethods) { + recipeUserIdToLoginMethodMap.put(loginMethod.getSupertokensUserId(), loginMethod); + } + + Map userIdToAuthRecipeUserInfo = new HashMap<>(); + + for (AllAuthRecipeUsersResultHolder authRecipeUsersResultHolder : allAuthUsersResult) { + String recipeUserId = authRecipeUsersResultHolder.userId; + LoginMethod loginMethod = recipeUserIdToLoginMethodMap.get(recipeUserId); + if (loginMethod == null) { + // loginMethod will be null for primaryUserId for which the user has been deleted during unlink + continue; + } + String primaryUserId = authRecipeUsersResultHolder.primaryOrRecipeUserId; + AuthRecipeUserInfo curr = userIdToAuthRecipeUserInfo.get(primaryUserId); + if (curr == null) { + curr = AuthRecipeUserInfo.create(primaryUserId, authRecipeUsersResultHolder.isLinkedOrIsAPrimaryUser, + loginMethod); + } else { + curr.addLoginMethod(loginMethod); + } + userIdToAuthRecipeUserInfo.put(primaryUserId, curr); } + + return userIdToAuthRecipeUserInfo.keySet().stream().map(userIdToAuthRecipeUserInfo::get) + .collect(Collectors.toList()); } - public static String getRecipeIdForUser_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + public static String getRecipeIdForUser_Transaction(Start start, Connection sqlCon, + TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { - ((ConnectionWithLocks) sqlCon).lock(tenantIdentifier.getAppId() + "~" + userId + Config.getConfig(start).getAppIdToUserIdTable()); + ((ConnectionWithLocks) sqlCon).lock( + tenantIdentifier.getAppId() + "~" + userId + Config.getConfig(start).getAppIdToUserIdTable()); String QUERY = "SELECT recipe_id FROM " + getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? AND user_id = ?"; - return execute(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, userId); @@ -860,12 +1387,14 @@ public static String getRecipeIdForUser_Transaction(Start start, Connection sqlC }); } - public static Map> getTenantIdsForUserIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String[] userIds) + public static Map> getTenantIdsForUserIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + String[] userIds) throws SQLException, StorageQueryException { if (userIds != null && userIds.length > 0) { StringBuilder QUERY = new StringBuilder("SELECT user_id, tenant_id " + "FROM " + getConfig(start).getUsersTable()); - QUERY.append(" WHERE app_id = ? AND user_id IN ("); + QUERY.append(" WHERE user_id IN ("); for (int i = 0; i < userIds.length; i++) { QUERY.append("?"); @@ -874,14 +1403,57 @@ public static Map> getTenantIdsForUserIds_transaction(Start QUERY.append(","); } } - QUERY.append(")"); + QUERY.append(") AND app_id = ?"); return execute(sqlCon, QUERY.toString(), pst -> { - pst.setString(1, appIdentifier.getAppId()); for (int i = 0; i < userIds.length; i++) { - // i+2 cause this starts with 1 and not 0, and 1 is appId - pst.setString(i + 2, userIds[i]); + // i+1 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 1, userIds[i]); + } + pst.setString(userIds.length + 1, appIdentifier.getAppId()); + }, result -> { + Map> finalResult = new HashMap<>(); + for (String userId : userIds) { + finalResult.put(userId, new ArrayList<>()); + } + + while (result.next()) { + String userId = result.getString("user_id").trim(); + String tenantId = result.getString("tenant_id"); + + finalResult.get(userId).add(tenantId); + } + return finalResult; + }); + } + + return new HashMap<>(); + } + + public static Map> getTenantIdsForUserIds(Start start, + AppIdentifier appIdentifier, + String[] userIds) + throws SQLException, StorageQueryException { + if (userIds != null && userIds.length > 0) { + StringBuilder QUERY = new StringBuilder("SELECT user_id, tenant_id " + + "FROM " + getConfig(start).getUsersTable()); + QUERY.append(" WHERE user_id IN ("); + for (int i = 0; i < userIds.length; i++) { + + QUERY.append("?"); + if (i != userIds.length - 1) { + // not the last element + QUERY.append(","); + } + } + QUERY.append(") AND app_id = ?"); + + return execute(start, QUERY.toString(), pst -> { + for (int i = 0; i < userIds.length; i++) { + // i+1 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 1, userIds[i]); } + pst.setString(userIds.length + 1, appIdentifier.getAppId()); }, result -> { Map> finalResult = new HashMap<>(); for (String userId : userIds) { @@ -909,7 +1481,7 @@ public static String[] getAllTablesInTheDatabase(Start start) throws SQLExceptio List tableNames = new ArrayList<>(); try (Connection con = ConnectionPool.getConnection(start)) { DatabaseMetaData metadata = con.getMetaData(); - ResultSet resultSet = metadata.getTables(null, null, null, new String[] { "TABLE" }); + ResultSet resultSet = metadata.getTables(null, null, null, new String[]{"TABLE"}); while (resultSet.next()) { String tableName = resultSet.getString("TABLE_NAME"); tableNames.add(tableName); @@ -944,13 +1516,89 @@ public static String[] getAllTablesInTheDatabaseThatHasDataForAppId(Start start, return result.toArray(new String[0]); } - private static class UserInfoPaginationResultHolder { - String userId; - String recipeId; + public static int getUsersCountWithMoreThanOneLoginMethod(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT (1) as c FROM (" + + " SELECT COUNT(user_id) as num_login_methods " + + " FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? " + + " GROUP BY (app_id, primary_or_recipe_user_id) " + + ") as nloginmethods WHERE num_login_methods > 1"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + }, result -> { + return result.next() ? result.getInt("c") : 0; + }); + } + + public static boolean checkIfUsesAccountLinking(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = "SELECT 1 FROM " + + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND is_linked_or_is_a_primary_user = true LIMIT 1"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + }, result -> { + return result.next(); + }); + } + + public static AccountLinkingInfo getAccountLinkingInfo_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { + GeneralQueries.AccountLinkingInfo accountLinkingInfo = new GeneralQueries.AccountLinkingInfo(userId, false); + { + String QUERY = "SELECT primary_or_recipe_user_id, is_linked_or_is_a_primary_user FROM " + + Config.getConfig(start).getAppIdToUserIdTable() + " WHERE app_id = ? AND user_id = ?"; + + accountLinkingInfo = execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + if (result.next()) { + String primaryUserId1 = result.getString("primary_or_recipe_user_id"); + boolean isLinked1 = result.getBoolean("is_linked_or_is_a_primary_user"); + return new AccountLinkingInfo(primaryUserId1, isLinked1); + + } + return null; + }); + } + return accountLinkingInfo; + } + + public static boolean doesUserIdExist_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { + // We query both tables cause there is a case where a primary user ID exists, but its associated + // recipe user ID has been deleted AND there are other recipe user IDs linked to this primary user ID already. + String QUERY = "SELECT 1 FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ? UNION SELECT 1 FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND primary_or_recipe_user_id = ?"; + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, appIdentifier.getAppId()); + pst.setString(4, userId); + }, ResultSet::next); + } - UserInfoPaginationResultHolder(String userId, String recipeId) { - this.userId = userId; - this.recipeId = recipeId; + private static class AllAuthRecipeUsersResultHolder { + String userId; + String tenantId; + String primaryOrRecipeUserId; + boolean isLinkedOrIsAPrimaryUser; + RECIPE_ID recipeId; + long timeJoined; + + AllAuthRecipeUsersResultHolder(String userId, String tenantId, String primaryOrRecipeUserId, + boolean isLinkedOrIsAPrimaryUser, String recipeId, long timeJoined) { + this.userId = userId.trim(); + this.tenantId = tenantId; + this.primaryOrRecipeUserId = primaryOrRecipeUserId; + this.isLinkedOrIsAPrimaryUser = isLinkedOrIsAPrimaryUser; + this.recipeId = RECIPE_ID.getEnumFromString(recipeId); + this.timeJoined = timeJoined; } } @@ -969,4 +1617,14 @@ public KeyValueInfo map(ResultSet result) throws Exception { return new KeyValueInfo(result.getString("value"), result.getLong("created_at_time")); } } + + public static class AccountLinkingInfo { + public String primaryUserId; + public boolean isLinked; + + public AccountLinkingInfo(String primaryUserId, boolean isLinked) { + this.primaryUserId = primaryUserId; + this.isLinked = isLinked; + } + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java index da667a45b..82af7379d 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java @@ -216,7 +216,7 @@ public static void addTenantIdInTargetStorage(Start start, TenantIdentifier tena { String QUERY = "INSERT INTO " + Config.getConfig(start).getTenantsTable() - + "(app_id, tenant_id, created_at_time)" + " VALUES(?, ?, ?) ON CONFLICT DO NOTHING"; + + "(app_id, tenant_id, created_at_time)" + " VALUES(?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); diff --git a/src/main/java/io/supertokens/inmemorydb/queries/PasswordlessQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/PasswordlessQueries.java index 7694189d8..54e5a49f3 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/PasswordlessQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/PasswordlessQueries.java @@ -19,15 +19,18 @@ import io.supertokens.inmemorydb.ConnectionPool; import io.supertokens.inmemorydb.ConnectionWithLocks; import io.supertokens.inmemorydb.Start; +import io.supertokens.inmemorydb.Utils; import io.supertokens.inmemorydb.config.Config; import io.supertokens.pluginInterface.RowMapper; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; -import io.supertokens.pluginInterface.passwordless.UserInfo; import io.supertokens.pluginInterface.sqlStorage.SQLStorage.TransactionIsolationLevel; import javax.annotation.Nonnull; @@ -36,6 +39,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute; import static io.supertokens.inmemorydb.QueryExecutorTemplate.update; @@ -69,7 +73,8 @@ static String getQueryToCreatePasswordlessUserToTenantTable(Start start) { + "UNIQUE (app_id, tenant_id, phone_number)," + "PRIMARY KEY (app_id, tenant_id, user_id)," + "FOREIGN KEY (app_id, tenant_id, user_id)" - + " REFERENCES " + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getUsersTable() + + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + ");"; // @formatter:on } @@ -85,7 +90,7 @@ public static String getQueryToCreateDevicesTable(Start start) { + "failed_attempts INT UNSIGNED NOT NULL," + "PRIMARY KEY (app_id, tenant_id, device_id_hash)," + "FOREIGN KEY(app_id, tenant_id)" - + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getTenantsTable() + " (app_id, tenant_id) ON DELETE CASCADE" + ");"; } @@ -98,8 +103,9 @@ public static String getQueryToCreateCodesTable(Start start) { + "link_code_hash CHAR(44) NOT NULL," + "created_at BIGINT UNSIGNED NOT NULL," + "PRIMARY KEY (app_id, tenant_id, code_id)," - + "UNIQUE (app_id, tenant_id, link_code_hash)," - + "FOREIGN KEY (app_id, tenant_id, device_id_hash) REFERENCES " + Config.getConfig(start).getPasswordlessDevicesTable() + + "UNIQUE (app_id, tenant_id, link_code_hash)," + + "FOREIGN KEY (app_id, tenant_id, device_id_hash) REFERENCES " + + Config.getConfig(start).getPasswordlessDevicesTable() + " (app_id, tenant_id, device_id_hash) ON DELETE CASCADE ON UPDATE CASCADE" + ");"; } @@ -111,7 +117,8 @@ public static String getQueryToCreateDeviceEmailIndex(Start start) { public static String getQueryToCreateDevicePhoneNumberIndex(Start start) { return "CREATE INDEX passwordless_devices_phone_number_index ON " - + Config.getConfig(start).getPasswordlessDevicesTable() + " (app_id, tenant_id, phone_number);"; // USING hash + + Config.getConfig(start).getPasswordlessDevicesTable() + + " (app_id, tenant_id, phone_number);"; // USING hash } public static String getQueryToCreateCodeDeviceIdHashIndex(Start start) { @@ -125,8 +132,10 @@ public static String getQueryToCreateCodeCreatedAtIndex(Start start) { } - public static void createDeviceWithCode(Start start, TenantIdentifier tenantIdentifier, String email, String phoneNumber, String linkCodeSalt, - PasswordlessCode code) throws StorageTransactionLogicException, StorageQueryException { + public static void createDeviceWithCode(Start start, TenantIdentifier tenantIdentifier, String email, + String phoneNumber, String linkCodeSalt, + PasswordlessCode code) + throws StorageTransactionLogicException, StorageQueryException { start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { @@ -155,7 +164,9 @@ public static PasswordlessDevice getDevice_Transaction(Start start, Connection c TenantIdentifier tenantIdentifier, String deviceIdHash) throws StorageQueryException, SQLException { - ((ConnectionWithLocks) con).lock(tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + deviceIdHash + Config.getConfig(start).getPasswordlessDevicesTable()); + ((ConnectionWithLocks) con).lock( + tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + deviceIdHash + + Config.getConfig(start).getPasswordlessDevicesTable()); String QUERY = "SELECT device_id_hash, email, phone_number, link_code_salt, failed_attempts FROM " + getConfig(start).getPasswordlessDevicesTable() @@ -173,7 +184,8 @@ public static PasswordlessDevice getDevice_Transaction(Start start, Connection c } public static void incrementDeviceFailedAttemptCount_Transaction(Start start, Connection con, - TenantIdentifier tenantIdentifier, String deviceIdHash) + TenantIdentifier tenantIdentifier, + String deviceIdHash) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getPasswordlessDevicesTable() + " SET failed_attempts = failed_attempts + 1" @@ -186,7 +198,8 @@ public static void incrementDeviceFailedAttemptCount_Transaction(Start start, Co }); } - public static void deleteDevice_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String deviceIdHash) + public static void deleteDevice_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + String deviceIdHash) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() + " WHERE app_id = ? AND tenant_id = ? AND device_id_hash = ?"; @@ -197,7 +210,9 @@ public static void deleteDevice_Transaction(Start start, Connection con, TenantI }); } - public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber) + public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, + TenantIdentifier tenantIdentifier, + @Nonnull String phoneNumber) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() @@ -210,7 +225,8 @@ public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connectio }); } - public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @Nonnull String phoneNumber, String userId) + public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + @Nonnull String phoneNumber, String userId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() @@ -227,7 +243,8 @@ public static void deleteDevicesByPhoneNumber_Transaction(Start start, Connectio }); } - public static void deleteDevicesByEmail_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, @Nonnull String email) + public static void deleteDevicesByEmail_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + @Nonnull String email) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() @@ -240,7 +257,8 @@ public static void deleteDevicesByEmail_Transaction(Start start, Connection con, }); } - public static void deleteDevicesByEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @Nonnull String email, String userId) + public static void deleteDevicesByEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + @Nonnull String email, String userId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessDevicesTable() @@ -257,7 +275,8 @@ public static void deleteDevicesByEmail_Transaction(Start start, Connection con, }); } - private static void createCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, PasswordlessCode code) + private static void createCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + PasswordlessCode code) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessCodesTable() + "(app_id, tenant_id, code_id, device_id_hash, link_code_hash, created_at)" @@ -287,7 +306,9 @@ public static void createCode(Start start, TenantIdentifier tenantIdentifier, Pa }); } - public static PasswordlessCode[] getCodesOfDevice_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String deviceIdHash) + public static PasswordlessCode[] getCodesOfDevice_Transaction(Start start, Connection con, + TenantIdentifier tenantIdentifier, + String deviceIdHash) throws StorageQueryException, SQLException { // We do not lock here, since the device is already locked earlier in the transaction. String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM " @@ -311,7 +332,9 @@ public static PasswordlessCode[] getCodesOfDevice_Transaction(Start start, Conne }); } - public static PasswordlessCode getCodeByLinkCodeHash_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String linkCodeHash) + public static PasswordlessCode getCodeByLinkCodeHash_Transaction(Start start, Connection con, + TenantIdentifier tenantIdentifier, + String linkCodeHash) throws StorageQueryException, SQLException { // We do not lock here, since the device is already locked earlier in the transaction. String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM " @@ -330,7 +353,8 @@ public static PasswordlessCode getCodeByLinkCodeHash_Transaction(Start start, Co }); } - public static void deleteCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String codeId) + public static void deleteCode_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, + String codeId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessCodesTable() + " WHERE app_id = ? AND tenant_id = ? AND code_id = ?"; @@ -342,30 +366,35 @@ public static void deleteCode_Transaction(Start start, Connection con, TenantIde }); } - public static UserInfo createUser(Start start, TenantIdentifier tenantIdentifier, String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined) + public static AuthRecipeUserInfo createUser(Start start, TenantIdentifier tenantIdentifier, String id, @Nullable String email, + @Nullable String phoneNumber, long timeJoined) throws StorageTransactionLogicException, StorageQueryException { return start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { { // app_id_to_user_id String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() - + "(app_id, user_id, recipe_id)" + " VALUES(?, ?, ?)"; + + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, id); - pst.setString(3, PASSWORDLESS.toString()); + pst.setString(3, id); + pst.setString(4, PASSWORDLESS.toString()); }); } { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, id); - pst.setString(4, PASSWORDLESS.toString()); - pst.setLong(5, timeJoined); + pst.setString(4, id); + pst.setString(5, PASSWORDLESS.toString()); + pst.setLong(6, timeJoined); + pst.setLong(7, timeJoined); }); } @@ -393,16 +422,20 @@ public static UserInfo createUser(Start start, TenantIdentifier tenantIdentifier pst.setString(5, phoneNumber); }); } - UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), new UserInfoPartial(id, email, phoneNumber, timeJoined)); + UserInfoPartial userInfo = new UserInfoPartial(id, email, phoneNumber, timeJoined); + fillUserInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); + fillUserInfoWithVerified_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); sqlCon.commit(); - return userInfo; + return AuthRecipeUserInfo.create(id, false, + userInfo.toLoginMethod()); } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); } }); } - private static UserInfoWithTenantId[] getUserInfosWithTenant(Start start, Connection con, AppIdentifier appIdentifier, String userId) + private static UserInfoWithTenantId[] getUserInfosWithTenant_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { String QUERY = "SELECT pl_users.user_id as user_id, pl_users.email as email, " + "pl_users.phone_number as phone_number, pl_users_to_tenant.tenant_id as tenant_id " @@ -429,49 +462,59 @@ private static UserInfoWithTenantId[] getUserInfosWithTenant(Start start, Connec }); } - public static void deleteUser(Start start, AppIdentifier appIdentifier, String userId) - throws StorageQueryException, StorageTransactionLogicException { - start.startTransaction(con -> { - Connection sqlCon = (Connection) con.getConnection(); - try { - UserInfoWithTenantId[] userInfos = getUserInfosWithTenant(start, sqlCon, appIdentifier, userId); + public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) + throws StorageQueryException, SQLException { + UserInfoWithTenantId[] userInfos = getUserInfosWithTenant_Transaction(start, sqlCon, appIdentifier, userId); - { - String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() - + " WHERE app_id = ? AND user_id = ?"; + if (deleteUserIdMappingToo) { + String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ?"; - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }); - } + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } else { + { + String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } - for (UserInfoWithTenantId userInfo : userInfos) { - if (userInfo.email != null) { - deleteDevicesByEmail_Transaction(start, sqlCon, - new TenantIdentifier( - appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), - userInfo.tenantId), - userInfo.email); - } - if (userInfo.phoneNumber != null) { - deleteDevicesByPhoneNumber_Transaction(start, sqlCon, - new TenantIdentifier( - appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), - userInfo.tenantId), - userInfo.phoneNumber); - } - } + { + String QUERY = "DELETE FROM " + getConfig(start).getPasswordlessUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + } - sqlCon.commit(); - } catch (SQLException throwables) { - throw new StorageTransactionLogicException(throwables); + for (UserInfoWithTenantId userInfo : userInfos) { + if (userInfo.email != null) { + deleteDevicesByEmail_Transaction(start, sqlCon, + new TenantIdentifier( + appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), + userInfo.tenantId), + userInfo.email); } - return null; - }); + if (userInfo.phoneNumber != null) { + deleteDevicesByPhoneNumber_Transaction(start, sqlCon, + new TenantIdentifier( + appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), + userInfo.tenantId), + userInfo.phoneNumber); + } + } } - public static int updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String email) + public static int updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String userId, String email) throws SQLException, StorageQueryException { { String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUserToTenantTable() @@ -495,7 +538,8 @@ public static int updateUserEmail_Transaction(Start start, Connection con, AppId } } - public static int updateUserPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, String phoneNumber) + public static int updateUserPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String userId, String phoneNumber) throws SQLException, StorageQueryException { { String QUERY = "UPDATE " + Config.getConfig(start).getPasswordlessUserToTenantTable() @@ -538,7 +582,8 @@ public static PasswordlessDevice getDevice(Start start, TenantIdentifier tenantI } } - public static PasswordlessDevice[] getDevicesByEmail(Start start, TenantIdentifier tenantIdentifier, @Nonnull String email) + public static PasswordlessDevice[] getDevicesByEmail(Start start, TenantIdentifier tenantIdentifier, + @Nonnull String email) throws StorageQueryException, SQLException { String QUERY = "SELECT device_id_hash, email, phone_number, link_code_salt, failed_attempts FROM " + getConfig(start).getPasswordlessDevicesTable() @@ -585,7 +630,8 @@ public static PasswordlessDevice[] getDevicesByPhoneNumber(Start start, TenantId }); } - public static PasswordlessCode[] getCodesOfDevice(Start start, TenantIdentifier tenantIdentifier, String deviceIdHash) + public static PasswordlessCode[] getCodesOfDevice(Start start, TenantIdentifier tenantIdentifier, + String deviceIdHash) throws StorageQueryException, SQLException { try (Connection con = ConnectionPool.getConnection(start)) { // We can call the transaction version here because it doesn't lock anything. @@ -593,7 +639,8 @@ public static PasswordlessCode[] getCodesOfDevice(Start start, TenantIdentifier } } - public static PasswordlessCode[] getCodesBefore(Start start, TenantIdentifier tenantIdentifier, long time) throws StorageQueryException, SQLException { + public static PasswordlessCode[] getCodesBefore(Start start, TenantIdentifier tenantIdentifier, long time) + throws StorageQueryException, SQLException { String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM " + getConfig(start).getPasswordlessCodesTable() + " WHERE app_id = ? AND tenant_id = ? AND created_at < ?"; @@ -615,7 +662,8 @@ public static PasswordlessCode[] getCodesBefore(Start start, TenantIdentifier te }); } - public static PasswordlessCode getCode(Start start, TenantIdentifier tenantIdentifier, String codeId) throws StorageQueryException, SQLException { + public static PasswordlessCode getCode(Start start, TenantIdentifier tenantIdentifier, String codeId) + throws StorageQueryException, SQLException { String QUERY = "SELECT code_id, device_id_hash, link_code_hash, created_at FROM " + getConfig(start).getPasswordlessCodesTable() + " WHERE app_id = ? AND tenant_id = ? AND code_id = ?"; @@ -632,7 +680,8 @@ public static PasswordlessCode getCode(Start start, TenantIdentifier tenantIdent }); } - public static PasswordlessCode getCodeByLinkCodeHash(Start start, TenantIdentifier tenantIdentifier, String linkCodeHash) + public static PasswordlessCode getCodeByLinkCodeHash(Start start, TenantIdentifier tenantIdentifier, + String linkCodeHash) throws StorageQueryException, SQLException { try (Connection con = ConnectionPool.getConnection(start)) { // We can call the transaction version here because it doesn't lock anything. @@ -640,28 +689,52 @@ public static PasswordlessCode getCodeByLinkCodeHash(Start start, TenantIdentifi } } - public static List getUsersByIdList(Start start, AppIdentifier appIdentifier, List ids) + public static List getUsersInfoUsingIdList(Start start, Set ids, + AppIdentifier appIdentifier) throws SQLException, StorageQueryException { if (ids.size() > 0) { // No need to filter based on tenantId because the id list is already filtered for a tenant - StringBuilder QUERY = new StringBuilder("SELECT user_id, email, phone_number, time_joined " - + "FROM " + getConfig(start).getPasswordlessUsersTable()); - QUERY.append(" WHERE app_id = ? AND user_id IN ("); - for (int i = 0; i < ids.size(); i++) { - QUERY.append("?"); - if (i != ids.size() - 1) { - // not the last element - QUERY.append(","); + String QUERY = "SELECT user_id, email, phone_number, time_joined " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " WHERE user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?"; + + List userInfos = execute(start, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; } - } - QUERY.append(")"); + pst.setString(index, appIdentifier.getAppId()); + }, result -> { + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); + } + return finalResult; + }); + fillUserInfoWithTenantIds(start, appIdentifier, userInfos); + fillUserInfoWithVerified(start, appIdentifier, userInfos); + return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList()); + } + return Collections.emptyList(); + } - List userInfos = execute(start, QUERY.toString(), pst -> { - pst.setString(1, appIdentifier.getAppId()); - for (int i = 0; i < ids.size(); i++) { - // i+2 cause this starts with 1 and not 0, and 1 is appId - pst.setString(i + 2, ids.get(i)); + public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids, + AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + if (ids.size() > 0) { + // No need to filter based on tenantId because the id list is already filtered for a tenant + String QUERY = "SELECT user_id, email, phone_number, time_joined " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " WHERE user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?"; + + List userInfos = execute(con, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; } + pst.setString(index, appIdentifier.getAppId()); }, result -> { List finalResult = new ArrayList<>(); while (result.next()) { @@ -669,16 +742,22 @@ public static List getUsersByIdList(Start start, AppIdentifier appIden } return finalResult; }); - return userInfoWithTenantIds(start, appIdentifier, userInfos); + fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos); + return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList()); } return Collections.emptyList(); } - public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { + private static UserInfoPartial getUserById_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, + String userId) + throws StorageQueryException, SQLException { + // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id + // table String QUERY = "SELECT user_id, email, phone_number, time_joined FROM " + getConfig(start).getPasswordlessUsersTable() + " WHERE app_id = ? AND user_id = ?"; - UserInfoPartial userInfo = execute(start, QUERY, pst -> { + return execute(sqlCon, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); }, result -> { @@ -687,92 +766,167 @@ public static UserInfo getUserById(Start start, AppIdentifier appIdentifier, Str } return null; }); - return userInfoWithTenantIds(start, appIdentifier, userInfo); } - public static UserInfoPartial getUserById(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { - // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id table - String QUERY = "SELECT user_id, email, phone_number, time_joined FROM " - + getConfig(start).getPasswordlessUsersTable() - + " WHERE app_id = ? AND user_id = ?"; + public static List lockEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String email) throws StorageQueryException, SQLException { + // normally the query below will use a for update, but sqlite doesn't support it. + ((ConnectionWithLocks) con).lock( + appIdentifier.getAppId() + "~" + email + + Config.getConfig(start).getPasswordlessUsersTable()); + String QUERY = "SELECT user_id FROM " + getConfig(start).getPasswordlessUsersTable() + + " WHERE app_id = ? AND email = ?"; + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } - return execute(sqlCon, QUERY, pst -> { + public static List lockPhone_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + String phoneNumber) + throws SQLException, StorageQueryException { + // normally the query below will use a for update, but sqlite doesn't support it. + ((ConnectionWithLocks) con).lock( + appIdentifier.getAppId() + "~" + phoneNumber + + Config.getConfig(start).getPasswordlessUsersTable()); + + String QUERY = "SELECT user_id FROM " + getConfig(start).getPasswordlessUsersTable() + + " WHERE app_id = ? AND phone_number = ?"; + return execute(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); + pst.setString(2, phoneNumber); }, result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); } - return null; + return userIds; }); } - public static UserInfo getUserByEmail(Start start, TenantIdentifier tenantIdentifier, @Nonnull String email) + public static String getPrimaryUserIdUsingEmail(Start start, TenantIdentifier tenantIdentifier, + String email) throws StorageQueryException, SQLException { - String QUERY = "SELECT pl_users.user_id as user_id, pl_users.email as email, " - + "pl_users.phone_number as phone_number, pl_users.time_joined as time_joined " - + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pl_users_to_tenant " - + "JOIN " + getConfig(start).getPasswordlessUsersTable() + " AS pl_users " - + "ON pl_users.app_id = pl_users_to_tenant.app_id AND pl_users.user_id = pl_users_to_tenant.user_id " - + "WHERE pl_users_to_tenant.app_id = ? AND pl_users_to_tenant.tenant_id = ? AND pl_users_to_tenant.email = ? "; + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pless" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" + + " WHERE pless.app_id = ? AND pless.tenant_id = ? AND pless.email = ?"; - UserInfoPartial userInfo = execute(start, QUERY, pst -> { + return execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, email); }, result -> { if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); + return result.getString("user_id"); } return null; }); - return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo); } - public static UserInfo getUserByPhoneNumber(Start start, TenantIdentifier tenantIdentifier, @Nonnull String phoneNumber) + public static List getPrimaryUserIdsUsingEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String email) throws StorageQueryException, SQLException { - String QUERY = "SELECT pl_users.user_id as user_id, pl_users.email as email, " - + "pl_users.phone_number as phone_number, pl_users.time_joined as time_joined " - + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pl_users_to_tenant " - + "JOIN " + getConfig(start).getPasswordlessUsersTable() + " AS pl_users " - + "ON pl_users.app_id = pl_users_to_tenant.app_id AND pl_users.user_id = pl_users_to_tenant.user_id " - + "WHERE pl_users_to_tenant.app_id = ? AND pl_users_to_tenant.tenant_id = ? AND pl_users_to_tenant.phone_number = ? "; + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pless" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" + + " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" + + " WHERE pless.app_id = ? AND pless.email = ?"; - UserInfoPartial userInfo = execute(start, QUERY, pst -> { + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + + public static String getPrimaryUserByPhoneNumber(Start start, TenantIdentifier tenantIdentifier, + @Nonnull String phoneNumber) + throws StorageQueryException, SQLException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getPasswordlessUserToTenantTable() + " AS pless" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" + + " WHERE pless.app_id = ? AND pless.tenant_id = ? AND pless.phone_number = ?"; + + return execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, phoneNumber); }, result -> { if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); + return result.getString("user_id"); } return null; }); - return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo); } - public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + public static List listUserIdsByPhoneNumber_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + @Nonnull String phoneNumber) throws StorageQueryException, SQLException { - UserInfoPartial userInfo = PasswordlessQueries.getUserById(start, sqlCon, + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getPasswordlessUsersTable() + " AS pless" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON pless.app_id = all_users.app_id AND pless.user_id = all_users.user_id" + + " WHERE pless.app_id = ? AND pless.phone_number = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, phoneNumber); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + + public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, + TenantIdentifier tenantIdentifier, String userId) + throws StorageQueryException, SQLException, UnknownUserIdException { + UserInfoPartial userInfo = PasswordlessQueries.getUserById_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + if (userInfo == null) { + throw new UnknownUserIdException(); + } + + GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, recipe_id, time_joined)" - + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, userInfo.id); - pst.setString(4, PASSWORDLESS.toString()); - pst.setLong(5, userInfo.timeJoined); + pst.setString(4, accountLinkingInfo.primaryUserId); + pst.setBoolean(5, accountLinkingInfo.isLinked); + pst.setString(6, PASSWORDLESS.toString()); + pst.setLong(7, userInfo.timeJoined); + pst.setLong(8, userInfo.timeJoined); }); } { // passwordless_user_to_tenant String QUERY = "INSERT INTO " + getConfig(start).getPasswordlessUserToTenantTable() + "(app_id, tenant_id, user_id, email, phone_number)" - + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT (app_id, tenant_id, user_id) DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); @@ -786,7 +940,8 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC } } - public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, + TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { { // all_auth_recipe_users String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() @@ -804,42 +959,124 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection // automatically deleted from passwordless_user_to_tenant because of foreign key constraint } - private static UserInfo userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, + Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, Arrays.asList(userInfo)).get(0); + return fillUserInfoWithVerified_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0); + } + + private static List fillUserInfoWithVerified_transaction(Start start, + Connection sqlCon, + AppIdentifier appIdentifier, + List userInfos) + throws SQLException, StorageQueryException { + List userIdsAndEmails = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + if (userInfo.email == null) { + // phone number, so we mark it as verified + userInfo.verified = true; + } else { + userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email)); + } + } + List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified_transaction(start, sqlCon, + appIdentifier, + userIdsAndEmails); + Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified); + for (UserInfoPartial userInfo : userInfos) { + if (userInfo.verified != null) { + // this means phone number + assert (userInfo.email == null); + continue; + } + if (verifiedUserIdsSet.contains(userInfo.id)) { + userInfo.verified = true; + } else { + userInfo.verified = false; + } } + return userInfos; } - private static List userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithVerified(Start start, + AppIdentifier appIdentifier, + List userInfos) throws SQLException, StorageQueryException { - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + List userIdsAndEmails = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + if (userInfo.email == null) { + // phone number, so we mark it as verified + userInfo.verified = true; + } else { + userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email)); + } } + List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified(start, + appIdentifier, + userIdsAndEmails); + Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified); + for (UserInfoPartial userInfo : userInfos) { + if (userInfo.verified != null) { + // this means phone number + assert (userInfo.email == null); + continue; + } + if (verifiedUserIdsSet.contains(userInfo.id)) { + userInfo.verified = true; + } else { + userInfo.verified = false; + } + } + return userInfos; } - private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static UserInfoPartial fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - return userInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0); + return fillUserInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0); } - private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userInfos) throws SQLException, StorageQueryException { String[] userIds = new String[userInfos.size()]; for (int i = 0; i < userInfos.size(); i++) { userIds[i] = userInfos.get(i).id; } - Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, appIdentifier, userIds); - List result = new ArrayList<>(); + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, + appIdentifier, + userIds); + List result = new ArrayList<>(); for (UserInfoPartial userInfo : userInfos) { - result.add(new UserInfo(userInfo.id, userInfo.email, userInfo.phoneNumber, userInfo.timeJoined, - tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]))); + userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]); + } + return userInfos; + } + + private static List fillUserInfoWithTenantIds(Start start, + AppIdentifier appIdentifier, + List userInfos) + throws SQLException, StorageQueryException { + String[] userIds = new String[userInfos.size()]; + for (int i = 0; i < userInfos.size(); i++) { + userIds[i] = userInfos.get(i).id; } - return result; + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds(start, + appIdentifier, + userIds); + List result = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]); + } + return userInfos; } private static class PasswordlessDeviceRowMapper implements RowMapper { @@ -882,6 +1119,9 @@ private static class UserInfoPartial { public final long timeJoined; public final String email; public final String phoneNumber; + public String[] tenantIds; + public Boolean verified; + public Boolean isPrimary; UserInfoPartial(String id, @Nullable String email, @Nullable String phoneNumber, long timeJoined) { this.id = id.trim(); @@ -894,6 +1134,13 @@ private static class UserInfoPartial { this.email = email; this.phoneNumber = phoneNumber; } + + public LoginMethod toLoginMethod() { + assert (tenantIds != null); + assert (verified != null); + return new LoginMethod(id, timeJoined, verified, new LoginMethod.PasswordlessInfo(email, phoneNumber), + tenantIds); + } } private static class UserInfoRowMapper implements RowMapper { @@ -913,7 +1160,6 @@ public UserInfoPartial map(ResultSet result) throws Exception { } } - private static class UserInfoWithTenantId { public final String userId; public final String tenantId; diff --git a/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java index 357985eeb..65fa18c1a 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java @@ -19,7 +19,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import io.supertokens.inmemorydb.ConnectionWithLocks; -import io.supertokens.inmemorydb.QueryExecutorTemplate; import io.supertokens.inmemorydb.Start; import io.supertokens.inmemorydb.config.Config; import io.supertokens.pluginInterface.KeyValueInfo; @@ -36,7 +35,6 @@ import java.util.ArrayList; import java.util.List; -import static io.supertokens.inmemorydb.PreparedStatementValueSetter.NO_OP_SETTER; import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute; import static io.supertokens.inmemorydb.QueryExecutorTemplate.update; import static io.supertokens.inmemorydb.config.Config.getConfig; @@ -105,21 +103,46 @@ public static SessionInfo getSessionInfo_Transaction(Start start, Connection con String sessionHandle) throws SQLException, StorageQueryException { - ((ConnectionWithLocks) con).lock(tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + sessionHandle + Config.getConfig(start).getSessionInfoTable()); - - String QUERY = "SELECT session_handle, user_id, refresh_token_hash_2, session_data, expires_at, " - + "created_at_time, jwt_user_payload, use_static_key FROM " + getConfig(start).getSessionInfoTable() - + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?"; - return execute(con, QUERY, pst -> { + ((ConnectionWithLocks) con).lock( + tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + sessionHandle + + Config.getConfig(start).getSessionInfoTable()); + // we do this as two separate queries and not one query with left join cause psql does not + // support left join with for update if the right table returns null. + String QUERY = + "SELECT session_handle, user_id, refresh_token_hash_2, session_data, " + + "expires_at, created_at_time, jwt_user_payload, use_static_key FROM " + + getConfig(start).getSessionInfoTable() + + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?"; + SessionInfo sessionInfo = execute(con, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, sessionHandle); }, result -> { if (result.next()) { - return SessionInfoRowMapper.getInstance().mapOrThrow(result); + return SessionInfoRowMapper.getInstance().mapOrThrow(result, false); } return null; }); + + if (sessionInfo == null) { + return null; + } + + QUERY = "SELECT primary_or_recipe_user_id FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getAppId()); + pst.setString(2, sessionInfo.recipeUserId); + }, result -> { + if (result.next()) { + String primaryUserId = result.getString("primary_or_recipe_user_id"); + if (primaryUserId != null) { + sessionInfo.userId = primaryUserId; + } + } + return sessionInfo; + }); } public static void updateSessionInfo_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, @@ -191,6 +214,18 @@ public static void deleteSessionsOfUser(Start start, AppIdentifier appIdentifier }); } + public static void deleteSessionsOfUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId) + throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getSessionInfoTable() + + " WHERE app_id = ? AND user_id = ?"; + + update(sqlCon, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + public static boolean deleteSessionsOfUser(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getSessionInfoTable() @@ -294,16 +329,24 @@ public static int updateSession(Start start, TenantIdentifier tenantIdentifier, public static SessionInfo getSession(Start start, TenantIdentifier tenantIdentifier, String sessionHandle) throws SQLException, StorageQueryException { - String QUERY = "SELECT session_handle, user_id, refresh_token_hash_2, session_data, expires_at, " - + "created_at_time, jwt_user_payload, use_static_key FROM " + getConfig(start).getSessionInfoTable() - + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?"; + String QUERY = + "SELECT sess.session_handle, sess.user_id, sess.refresh_token_hash_2, sess.session_data, sess" + + ".expires_at, " + + + "sess.created_at_time, sess.jwt_user_payload, sess.use_static_key, users" + + ".primary_or_recipe_user_id FROM " + + getConfig(start).getSessionInfoTable() + + " AS sess LEFT JOIN " + getConfig(start).getUsersTable() + + " as users ON sess.app_id = users.app_id AND sess.user_id = users.user_id WHERE sess.app_id =" + + " ? AND " + + "sess.tenant_id = ? AND sess.session_handle = ?"; return execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, sessionHandle); }, result -> { if (result.next()) { - return SessionInfoRowMapper.getInstance().mapOrThrow(result); + return SessionInfoRowMapper.getInstance().mapOrThrow(result, true); } return null; }); @@ -326,7 +369,8 @@ public static void addAccessTokenSigningKey_Transaction(Start start, Connection public static KeyValueInfo[] getAccessTokenSigningKeys_Transaction(Start start, Connection con, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { - ((ConnectionWithLocks) con).lock(appIdentifier.getAppId() + Config.getConfig(start).getAccessTokenSigningKeysTable()); + ((ConnectionWithLocks) con).lock( + appIdentifier.getAppId() + Config.getConfig(start).getAccessTokenSigningKeysTable()); String QUERY = "SELECT * FROM " + getConfig(start).getAccessTokenSigningKeysTable() + " WHERE app_id = ?"; @@ -357,7 +401,7 @@ public static void removeAccessTokenSigningKeysBefore(Start start, AppIdentifier }); } - static class SessionInfoRowMapper implements RowMapper { + static class SessionInfoRowMapper { public static final SessionInfoRowMapper INSTANCE = new SessionInfoRowMapper(); private SessionInfoRowMapper() { @@ -367,14 +411,23 @@ private static SessionInfoRowMapper getInstance() { return INSTANCE; } - @Override - public SessionInfo map(ResultSet result) throws Exception { + public SessionInfo mapOrThrow(ResultSet result, boolean hasPrimaryOrRecipeUserId) throws StorageQueryException { JsonParser jp = new JsonParser(); - return new SessionInfo(result.getString("session_handle"), result.getString("user_id"), - result.getString("refresh_token_hash_2"), - jp.parse(result.getString("session_data")).getAsJsonObject(), result.getLong("expires_at"), - jp.parse(result.getString("jwt_user_payload")).getAsJsonObject(), - result.getLong("created_at_time"), result.getBoolean("use_static_key")); + // if result.getString("primary_or_recipe_user_id") is null, it will be handled by SessionInfo + // constructor + try { + return new SessionInfo(result.getString("session_handle"), + hasPrimaryOrRecipeUserId ? result.getString("primary_or_recipe_user_id") : + result.getString("user_id"), + result.getString("user_id"), + result.getString("refresh_token_hash_2"), + jp.parse(result.getString("session_data")).getAsJsonObject(), + result.getLong("expires_at"), + jp.parse(result.getString("jwt_user_payload")).getAsJsonObject(), + result.getLong("created_at_time"), result.getBoolean("use_static_key")); + } catch (Exception e) { + throw new StorageQueryException(e); + } } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/ThirdPartyQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/ThirdPartyQueries.java index 5ef6ee4d8..3e3848353 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/ThirdPartyQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/ThirdPartyQueries.java @@ -19,19 +19,22 @@ import io.supertokens.inmemorydb.ConnectionPool; import io.supertokens.inmemorydb.ConnectionWithLocks; import io.supertokens.inmemorydb.Start; +import io.supertokens.inmemorydb.Utils; import io.supertokens.inmemorydb.config.Config; import io.supertokens.pluginInterface.RowMapper; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.pluginInterface.thirdparty.UserInfo; -import org.jetbrains.annotations.NotNull; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute; import static io.supertokens.inmemorydb.QueryExecutorTemplate.update; @@ -76,35 +79,41 @@ static String getQueryToCreateThirdPartyUserToTenantTable(Start start) { + "UNIQUE (app_id, tenant_id, third_party_id, third_party_user_id)," + "PRIMARY KEY (app_id, tenant_id, user_id)," + "FOREIGN KEY (app_id, tenant_id, user_id)" - + " REFERENCES " + Config.getConfig(start).getUsersTable() + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + + " REFERENCES " + Config.getConfig(start).getUsersTable() + + "(app_id, tenant_id, user_id) ON DELETE CASCADE" + ");"; // @formatter:on } - public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String id, String email, UserInfo.ThirdParty thirdParty, long timeJoined) + public static AuthRecipeUserInfo signUp(Start start, TenantIdentifier tenantIdentifier, String id, String email, + LoginMethod.ThirdParty thirdParty, long timeJoined) throws StorageQueryException, StorageTransactionLogicException { return start.startTransaction(con -> { Connection sqlCon = (Connection) con.getConnection(); try { { // app_id_to_user_id String QUERY = "INSERT INTO " + getConfig(start).getAppIdToUserIdTable() - + "(app_id, user_id, recipe_id)" + " VALUES(?, ?, ?)"; + + "(app_id, user_id, primary_or_recipe_user_id, recipe_id)" + " VALUES(?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, id); - pst.setString(3, THIRD_PARTY.toString()); + pst.setString(3, id); + pst.setString(4, THIRD_PARTY.toString()); }); } { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, recipe_id, time_joined)" + " VALUES(?, ?, ?, ?, ?)"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?, ?)"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, id); - pst.setString(4, THIRD_PARTY.toString()); - pst.setLong(5, timeJoined); + pst.setString(4, id); + pst.setString(5, THIRD_PARTY.toString()); + pst.setLong(6, timeJoined); + pst.setLong(7, timeJoined); }); } @@ -135,9 +144,11 @@ public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, St }); } - UserInfo userInfo = userInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), new UserInfoPartial(id, email, thirdParty, timeJoined)); + UserInfoPartial userInfo = new UserInfoPartial(id, email, thirdParty, timeJoined); + fillUserInfoWithTenantIds_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); + fillUserInfoWithVerified_transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userInfo); sqlCon.commit(); - return userInfo; + return AuthRecipeUserInfo.create(id, false, userInfo.toLoginMethod()); } catch (SQLException throwables) { throw new StorageTransactionLogicException(throwables); @@ -145,70 +156,136 @@ public static UserInfo signUp(Start start, TenantIdentifier tenantIdentifier, St }); } - public static void deleteUser(Start start, AppIdentifier appIdentifier, String userId) - throws StorageQueryException, StorageTransactionLogicException { - start.startTransaction(con -> { - Connection sqlCon = (Connection) con.getConnection(); - try { - { - String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() - + " WHERE app_id = ? AND user_id = ?"; + public static void deleteUser_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId, boolean deleteUserIdMappingToo) + throws StorageQueryException, SQLException { + if (deleteUserIdMappingToo) { + String QUERY = "DELETE FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND user_id = ?"; - update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }); - } + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } else { + { + String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } - sqlCon.commit(); - } catch (SQLException throwables) { - throw new StorageTransactionLogicException(throwables); + { + String QUERY = "DELETE FROM " + getConfig(start).getThirdPartyUsersTable() + + " WHERE app_id = ? AND user_id = ?"; + update(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); } - return null; + } + } + + public static List lockEmail_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + String email) throws SQLException, StorageQueryException { + // normally the query below will use a for update, but sqlite doesn't support it. + ((ConnectionWithLocks) con).lock( + appIdentifier.getAppId() + "~" + email + + Config.getConfig(start).getThirdPartyUsersTable()); + + String QUERY = "SELECT tp.user_id as user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " WHERE tp.app_id = ? AND tp.email = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(result.getString("user_id")); + } + return finalResult; }); } - public static UserInfo getThirdPartyUserInfoUsingId(Start start, AppIdentifier appIdentifier, String userId) + public static List lockThirdPartyInfo_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, + String thirdPartyId, String thirdPartyUserId) throws SQLException, StorageQueryException { - String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " - + getConfig(start).getThirdPartyUsersTable() + " WHERE app_id = ? AND user_id = ?"; + // normally the query below will use a for update, but sqlite doesn't support it. + ((ConnectionWithLocks) con).lock( + appIdentifier.getAppId() + "~" + thirdPartyId + thirdPartyUserId + + Config.getConfig(start).getThirdPartyUsersTable()); + + // in psql / mysql dbs, this will lock the rows that are in both the tables that meet the ON criteria only. + String QUERY = "SELECT user_id " + + " FROM " + getConfig(start).getThirdPartyUsersTable() + + " WHERE app_id = ? AND third_party_id = ? AND third_party_user_id = ?"; - UserInfoPartial userInfo = execute(start, QUERY.toString(), pst -> { + return execute(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); + pst.setString(2, thirdPartyId); + pst.setString(3, thirdPartyUserId); }, result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(result.getString("user_id")); } - return null; + return finalResult; }); - return userInfoWithTenantIds(start, appIdentifier, userInfo); } - public static List getUsersInfoUsingIdList(Start start, AppIdentifier appIdentifier, List ids) + public static List getUsersInfoUsingIdList(Start start, Set ids, + AppIdentifier appIdentifier) throws SQLException, StorageQueryException { if (ids.size() > 0) { - // No need to filter based on tenantId because the id list is already filtered for a tenant - StringBuilder QUERY = new StringBuilder( - "SELECT user_id, third_party_id, third_party_user_id, email, time_joined " - + "FROM " + getConfig(start).getThirdPartyUsersTable()); - QUERY.append(" WHERE app_id = ? AND user_id IN ("); - for (int i = 0; i < ids.size(); i++) { - - QUERY.append("?"); - if (i != ids.size() - 1) { - // not the last element - QUERY.append(","); + String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " WHERE user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?"; + + List userInfos = execute(start, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; + } + pst.setString(index, appIdentifier.getAppId()); + }, result -> { + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); } + return finalResult; + }); + + try (Connection con = ConnectionPool.getConnection(start)) { + fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos); } - QUERY.append(")"); + return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList()); + } + return Collections.emptyList(); + } - List userInfos = execute(start, QUERY.toString(), pst -> { - pst.setString(1, appIdentifier.getAppId()); - for (int i = 0; i < ids.size(); i++) { - // i+2 cause this starts with 1 and not 0, and 1 is appId - pst.setString(i + 2, ids.get(i)); + public static List getUsersInfoUsingIdList_Transaction(Start start, Connection con, Set ids, + AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + if (ids.size() > 0) { + String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " WHERE user_id IN (" + + Utils.generateCommaSeperatedQuestionMarks(ids.size()) + ") AND app_id = ?"; + + List userInfos = execute(con, QUERY, pst -> { + int index = 1; + for (String id : ids) { + pst.setString(index, id); + index++; } + pst.setString(index, appIdentifier.getAppId()); }, result -> { List finalResult = new ArrayList<>(); while (result.next()) { @@ -216,36 +293,82 @@ public static List getUsersInfoUsingIdList(Start start, AppIdentifier } return finalResult; }); - return userInfoWithTenantIds(start, appIdentifier, userInfos); + + fillUserInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + fillUserInfoWithVerified_transaction(start, con, appIdentifier, userInfos); + return userInfos.stream().map(UserInfoPartial::toLoginMethod).collect(Collectors.toList()); } return Collections.emptyList(); } - public static UserInfo getThirdPartyUserInfoUsingId(Start start, TenantIdentifier tenantIdentifier, - String thirdPartyId, String thirdPartyUserId) + + public static List listUserIdsByThirdPartyInfo(Start start, AppIdentifier appIdentifier, + String thirdPartyId, String thirdPartyUserId) throws SQLException, StorageQueryException { - String QUERY = "SELECT tp_users.user_id as user_id, tp_users.third_party_id as third_party_id, " - + "tp_users.third_party_user_id as third_party_user_id, tp_users.email as email, " - + "tp_users.time_joined as time_joined " - + "FROM " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp_users_to_tenant " - + "JOIN " + getConfig(start).getThirdPartyUsersTable() + " AS tp_users " - + "ON tp_users.app_id = tp_users_to_tenant.app_id AND tp_users.user_id = tp_users_to_tenant.user_id " - + "WHERE tp_users_to_tenant.app_id = ? AND tp_users_to_tenant.tenant_id = ? " - + "AND tp_users_to_tenant.third_party_id = ? AND tp_users_to_tenant.third_party_user_id = ?"; + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" + + " WHERE tp.app_id = ? AND tp.third_party_id = ? AND tp.third_party_user_id = ?"; - UserInfoPartial userInfo = execute(start, QUERY, pst -> { + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, thirdPartyId); + pst.setString(3, thirdPartyUserId); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + + public static List listUserIdsByThirdPartyInfo_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String thirdPartyId, String thirdPartyUserId) + throws SQLException, StorageQueryException { + + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" + + " WHERE tp.app_id = ? AND tp.third_party_id = ? AND tp.third_party_user_id = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, thirdPartyId); + pst.setString(3, thirdPartyUserId); + }, result -> { + List userIds = new ArrayList<>(); + while (result.next()) { + userIds.add(result.getString("user_id")); + } + return userIds; + }); + } + + public static String getUserIdByThirdPartyInfo(Start start, TenantIdentifier tenantIdentifier, + String thirdPartyId, String thirdPartyUserId) + throws SQLException, StorageQueryException { + + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" + + " WHERE tp.app_id = ? AND tp.tenant_id = ? AND tp.third_party_id = ? AND tp.third_party_user_id = ?"; + + return execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, thirdPartyId); pst.setString(4, thirdPartyUserId); }, result -> { if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); + return result.getString("user_id"); } return null; }); - return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfo); } public static void updateUserEmail_Transaction(Start start, Connection con, AppIdentifier appIdentifier, @@ -262,34 +385,12 @@ public static void updateUserEmail_Transaction(Start start, Connection con, AppI }); } - public static UserInfo getUserInfoUsingId_Transaction(Start start, Connection con, - AppIdentifier appIdentifier, String thirdPartyId, - String thirdPartyUserId) - throws SQLException, StorageQueryException { - - ((ConnectionWithLocks) con).lock(appIdentifier.getAppId() + "~" + thirdPartyId + "~" + thirdPartyUserId + Config.getConfig(start).getThirdPartyUsersTable()); - - String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " - + getConfig(start).getThirdPartyUsersTable() - + " WHERE app_id = ? AND third_party_id = ? AND third_party_user_id = ?"; - UserInfoPartial userInfo = execute(con, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, thirdPartyId); - pst.setString(3, thirdPartyUserId); - }, result -> { - if (result.next()) { - return UserInfoRowMapper.getInstance().mapOrThrow(result); - } - return null; - }); - return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfo); - } - - private static UserInfoPartial getUserInfoUsingUserId(Start start, Connection con, + private static UserInfoPartial getUserInfoUsingUserId_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { - // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id table + // we don't need a LOCK here because this is already part of a transaction, and locked on app_id_to_user_id + // table String QUERY = "SELECT user_id, third_party_id, third_party_user_id, email, time_joined FROM " + getConfig(start).getThirdPartyUsersTable() + " WHERE app_id = ? AND user_id = ?"; @@ -304,55 +405,83 @@ private static UserInfoPartial getUserInfoUsingUserId(Start start, Connection co }); } - public static UserInfo[] getThirdPartyUsersByEmail(Start start, TenantIdentifier tenantIdentifier, - @NotNull String email) - throws SQLException, StorageQueryException { - - String QUERY = "SELECT tp_users.user_id as user_id, tp_users.third_party_id as third_party_id, " - + "tp_users.third_party_user_id as third_party_user_id, tp_users.email as email, " - + "tp_users.time_joined as time_joined " - + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp_users " - + "JOIN " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp_users_to_tenant " - + "ON tp_users.app_id = tp_users_to_tenant.app_id AND tp_users.user_id = tp_users_to_tenant.user_id " - + "WHERE tp_users_to_tenant.app_id = ? AND tp_users_to_tenant.tenant_id = ? AND tp_users.email = ? " - + "ORDER BY time_joined"; - - List userInfos = execute(start, QUERY.toString(), pst -> { + public static List getPrimaryUserIdUsingEmail(Start start, + TenantIdentifier tenantIdentifier, String email) + throws StorageQueryException, SQLException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " JOIN " + getConfig(start).getUsersTable() + " AS all_users" + + " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" + + " JOIN " + getConfig(start).getThirdPartyUserToTenantTable() + " AS tp_tenants" + + " ON tp_tenants.app_id = all_users.app_id AND tp_tenants.user_id = all_users.user_id" + + " WHERE tp.app_id = ? AND tp_tenants.tenant_id = ? AND tp.email = ?"; + + return execute(start, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, email); }, result -> { - List finalResult = new ArrayList<>(); + List finalResult = new ArrayList<>(); while (result.next()) { - finalResult.add(UserInfoRowMapper.getInstance().mapOrThrow(result)); + finalResult.add(result.getString("user_id")); } return finalResult; }); - return userInfoWithTenantIds(start, tenantIdentifier.toAppIdentifier(), userInfos).toArray(new UserInfo[0]); } - public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) - throws SQLException, StorageQueryException { - UserInfoPartial userInfo = ThirdPartyQueries.getUserInfoUsingUserId(start, sqlCon, + public static List getPrimaryUserIdUsingEmail_Transaction(Start start, Connection con, + AppIdentifier appIdentifier, String email) + throws StorageQueryException, SQLException { + String QUERY = "SELECT DISTINCT all_users.primary_or_recipe_user_id AS user_id " + + "FROM " + getConfig(start).getThirdPartyUsersTable() + " AS tp" + + " JOIN " + getConfig(start).getAppIdToUserIdTable() + " AS all_users" + + " ON tp.app_id = all_users.app_id AND tp.user_id = all_users.user_id" + + " WHERE tp.app_id = ? AND tp.email = ?"; + + return execute(con, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, email); + }, result -> { + List finalResult = new ArrayList<>(); + while (result.next()) { + finalResult.add(result.getString("user_id")); + } + return finalResult; + }); + } + + public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlCon, + TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException, UnknownUserIdException { + UserInfoPartial userInfo = ThirdPartyQueries.getUserInfoUsingUserId_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + if (userInfo == null) { + throw new UnknownUserIdException(); + } + + GeneralQueries.AccountLinkingInfo accountLinkingInfo = GeneralQueries.getAccountLinkingInfo_Transaction(start, sqlCon, tenantIdentifier.toAppIdentifier(), userId); + { // all_auth_recipe_users String QUERY = "INSERT INTO " + getConfig(start).getUsersTable() - + "(app_id, tenant_id, user_id, recipe_id, time_joined)" - + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + + "(app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined)" + + " VALUES(?, ?, ?, ?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); pst.setString(3, userInfo.id); - pst.setString(4, THIRD_PARTY.toString()); - pst.setLong(5, userInfo.timeJoined); + pst.setString(4, accountLinkingInfo.primaryUserId); + pst.setBoolean(5, accountLinkingInfo.isLinked); + pst.setString(6, THIRD_PARTY.toString()); + pst.setLong(7, userInfo.timeJoined); + pst.setLong(8, userInfo.timeJoined); }); } { // thirdparty_user_to_tenant String QUERY = "INSERT INTO " + getConfig(start).getThirdPartyUserToTenantTable() + "(app_id, tenant_id, user_id, third_party_id, third_party_user_id)" - + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT DO NOTHING"; + + " VALUES(?, ?, ?, ?, ?)" + " ON CONFLICT (app_id, tenant_id, user_id) DO NOTHING"; int numRows = update(sqlCon, QUERY, pst -> { pst.setString(1, tenantIdentifier.getAppId()); pst.setString(2, tenantIdentifier.getTenantId()); @@ -365,7 +494,8 @@ public static boolean addUserIdToTenant_Transaction(Start start, Connection sqlC } } - public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String userId) + public static boolean removeUserIdFromTenant_Transaction(Start start, Connection sqlCon, + TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { { // all_auth_recipe_users String QUERY = "DELETE FROM " + getConfig(start).getUsersTable() @@ -383,56 +513,84 @@ public static boolean removeUserIdFromTenant_Transaction(Start start, Connection // automatically deleted from thirdparty_user_to_tenant because of foreign key constraint } - private static UserInfo userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static UserInfoPartial fillUserInfoWithVerified_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, Arrays.asList(userInfo)).get(0); - } + return fillUserInfoWithVerified_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0); } - private static List userInfoWithTenantIds(Start start, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithVerified_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userInfos) throws SQLException, StorageQueryException { - try (Connection con = ConnectionPool.getConnection(start)) { - return userInfoWithTenantIds_transaction(start, con, appIdentifier, userInfos); + List userIdsAndEmails = new ArrayList<>(); + for (UserInfoPartial userInfo : userInfos) { + userIdsAndEmails.add(new EmailVerificationQueries.UserIdAndEmail(userInfo.id, userInfo.email)); + } + List userIdsThatAreVerified = EmailVerificationQueries.isEmailVerified_transaction(start, sqlCon, + appIdentifier, + userIdsAndEmails); + Set verifiedUserIdsSet = new HashSet<>(userIdsThatAreVerified); + for (UserInfoPartial userInfo : userInfos) { + if (verifiedUserIdsSet.contains(userInfo.id)) { + userInfo.verified = true; + } else { + userInfo.verified = false; + } } + return userInfos; } - private static UserInfo userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, UserInfoPartial userInfo) + private static UserInfoPartial fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + UserInfoPartial userInfo) throws SQLException, StorageQueryException { if (userInfo == null) return null; - return userInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, Arrays.asList(userInfo)).get(0); + return fillUserInfoWithTenantIds_transaction(start, sqlCon, appIdentifier, List.of(userInfo)).get(0); } - private static List userInfoWithTenantIds_transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, List userInfos) + private static List fillUserInfoWithTenantIds_transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userInfos) throws SQLException, StorageQueryException { String[] userIds = new String[userInfos.size()]; for (int i = 0; i < userInfos.size(); i++) { userIds[i] = userInfos.get(i).id; } - Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, appIdentifier, userIds); - List result = new ArrayList<>(); + Map> tenantIdsForUserIds = GeneralQueries.getTenantIdsForUserIds_transaction(start, sqlCon, + appIdentifier, + userIds); for (UserInfoPartial userInfo : userInfos) { - result.add(new UserInfo(userInfo.id, userInfo.email, userInfo.thirdParty, userInfo.timeJoined, - tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]))); + userInfo.tenantIds = tenantIdsForUserIds.get(userInfo.id).toArray(new String[0]); } - - return result; + return userInfos; } private static class UserInfoPartial { public final String id; public final String email; - public final UserInfo.ThirdParty thirdParty; + public final LoginMethod.ThirdParty thirdParty; public final long timeJoined; + public String[] tenantIds; + public Boolean verified; + public Boolean isPrimary; - public UserInfoPartial(String id, String email, UserInfo.ThirdParty thirdParty, long timeJoined) { + public UserInfoPartial(String id, String email, LoginMethod.ThirdParty thirdParty, long timeJoined) { this.id = id.trim(); this.email = email; this.thirdParty = thirdParty; this.timeJoined = timeJoined; } + + public LoginMethod toLoginMethod() { + assert (tenantIds != null); + assert (verified != null); + return new LoginMethod(id, timeJoined, verified, email, + new LoginMethod.ThirdParty(thirdParty.id, thirdParty.userId), tenantIds); + } } private static class UserInfoRowMapper implements RowMapper { @@ -448,7 +606,7 @@ private static UserInfoRowMapper getInstance() { @Override public UserInfoPartial map(ResultSet result) throws Exception { return new UserInfoPartial(result.getString("user_id"), result.getString("email"), - new UserInfo.ThirdParty(result.getString("third_party_id"), + new LoginMethod.ThirdParty(result.getString("third_party_id"), result.getString("third_party_user_id")), result.getLong("time_joined")); } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java index fd7831c75..2bfad8270 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java @@ -24,6 +24,7 @@ import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import javax.annotation.Nullable; +import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -115,6 +116,25 @@ public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExter } + public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + + " WHERE app_id = ? AND (supertokens_user_id = ? OR external_user_id = ?)"; + + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, userId); + }, result -> { + ArrayList userIdMappingArray = new ArrayList<>(); + while (result.next()) { + userIdMappingArray.add(UserIdMappingRowMapper.getInstance().mapOrThrow(result)); + } + return userIdMappingArray.toArray(UserIdMapping[]::new); + }); + + } + public static HashMap getUserIdMappingWithUserIds(Start start, ArrayList userIds) throws SQLException, StorageQueryException { @@ -205,6 +225,37 @@ public static boolean updateOrDeleteExternalUserIdInfoWithExternalUserId(Start s return rowUpdated > 0; } + public static UserIdMapping getuseraIdMappingWithSuperTokensUserId_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + + " WHERE app_id = ? AND supertokens_user_id = ?"; + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + if (result.next()) { + return UserIdMappingRowMapper.getInstance().mapOrThrow(result); + } + return null; + }); + } + + public static UserIdMapping getUserIdMappingWithExternalUserId_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { + String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + + " WHERE app_id = ? AND external_user_id = ?"; + + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }, result -> { + if (result.next()) { + return UserIdMappingRowMapper.getInstance().mapOrThrow(result); + } + return null; + }); + } + private static class UserIdMappingRowMapper implements RowMapper { private static final UserIdMappingRowMapper INSTANCE = new UserIdMappingRowMapper(); @@ -221,4 +272,5 @@ public UserIdMapping map(ResultSet rs) throws Exception { rs.getString("external_user_id_info")); } } -} \ No newline at end of file + +} diff --git a/src/main/java/io/supertokens/inmemorydb/queries/UserMetadataQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/UserMetadataQueries.java index 2fb93b620..bbdb4d9c3 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/UserMetadataQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/UserMetadataQueries.java @@ -48,7 +48,8 @@ public static String getQueryToCreateUserMetadataTable(Start start) { // @formatter:on } - public static int deleteUserMetadata(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + public static int deleteUserMetadata(Start start, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getUserMetadataTable() + " WHERE app_id = ? AND user_id = ?"; @@ -58,7 +59,20 @@ public static int deleteUserMetadata(Start start, AppIdentifier appIdentifier, S }); } - public static int setUserMetadata_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String userId, JsonObject metadata) + public static int deleteUserMetadata_Transaction(Connection sqlCon, Start start, AppIdentifier appIdentifier, + String userId) + throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getUserMetadataTable() + + " WHERE app_id = ? AND user_id = ?"; + + return update(sqlCon, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + }); + } + + public static int setUserMetadata_Transaction(Start start, Connection con, AppIdentifier appIdentifier, + String userId, JsonObject metadata) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getUserMetadataTable() @@ -91,7 +105,8 @@ public static JsonObject getUserMetadata_Transaction(Start start, Connection con }); } - public static JsonObject getUserMetadata(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + public static JsonObject getUserMetadata(Start start, AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { String QUERY = "SELECT user_metadata FROM " + getConfig(start).getUserMetadataTable() + " WHERE app_id = ? AND user_id = ?"; return execute(start, QUERY, pst -> { diff --git a/src/main/java/io/supertokens/inmemorydb/queries/UserRoleQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/UserRolesQueries.java similarity index 88% rename from src/main/java/io/supertokens/inmemorydb/queries/UserRoleQueries.java rename to src/main/java/io/supertokens/inmemorydb/queries/UserRolesQueries.java index 028a1e459..dc7fb1710 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/UserRoleQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/UserRolesQueries.java @@ -33,7 +33,7 @@ import static io.supertokens.inmemorydb.QueryExecutorTemplate.update; import static io.supertokens.inmemorydb.config.Config.getConfig; -public class UserRoleQueries { +public class UserRolesQueries { public static String getQueryToCreateRolesTable(Start start) { String tableName = Config.getConfig(start).getRolesTable(); // @formatter:off @@ -101,7 +101,8 @@ public static boolean createNewRoleOrDoNothingIfExists_Transaction(Start start, public static void addPermissionToRoleOrDoNothingIfExists_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String role, - String permission) throws SQLException, StorageQueryException { + String permission) + throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + getConfig(start).getUserRolesPermissionsTable() + " (app_id, role, permission) VALUES(?, ?, ?) ON CONFLICT DO NOTHING"; @@ -112,32 +113,18 @@ public static void addPermissionToRoleOrDoNothingIfExists_Transaction(Start star }); } - public static boolean deleteRole(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { - - try { - return start.startTransaction(con -> { - // Row lock must be taken to delete the role, otherwise the table may be locked for delete - Connection sqlCon = (Connection) con.getConnection(); - ((ConnectionWithLocks) sqlCon).lock(appIdentifier.getAppId() + "~" + role + Config.getConfig(start).getRolesTable()); - - String QUERY = "DELETE FROM " + getConfig(start).getRolesTable() - + " WHERE app_id = ? AND role = ? ;"; - - try { - return update(sqlCon, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, role); - }) == 1; - } catch (SQLException e) { - throw new StorageTransactionLogicException(e); - } - }); - } catch (StorageTransactionLogicException e) { - throw new StorageQueryException(e.actualException); - } + public static boolean deleteRole(Start start, AppIdentifier appIdentifier, String role) + throws SQLException, StorageQueryException { + String QUERY = "DELETE FROM " + getConfig(start).getRolesTable() + + " WHERE app_id = ? AND role = ? ;"; + return update(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, role); + }) == 1; } - public static boolean doesRoleExist(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { + public static boolean doesRoleExist(Start start, AppIdentifier appIdentifier, String role) + throws SQLException, StorageQueryException { String QUERY = "SELECT 1 FROM " + getConfig(start).getRolesTable() + " WHERE app_id = ? AND role = ?"; return execute(start, QUERY, pst -> { @@ -146,7 +133,8 @@ public static boolean doesRoleExist(Start start, AppIdentifier appIdentifier, St }, ResultSet::next); } - public static String[] getPermissionsForRole(Start start, AppIdentifier appIdentifier, String role) throws SQLException, StorageQueryException { + public static String[] getPermissionsForRole(Start start, AppIdentifier appIdentifier, String role) + throws SQLException, StorageQueryException { String QUERY = "SELECT permission FROM " + Config.getConfig(start).getUserRolesPermissionsTable() + " WHERE app_id = ? AND role = ?;"; return execute(start, QUERY, pst -> { @@ -161,7 +149,8 @@ public static String[] getPermissionsForRole(Start start, AppIdentifier appIdent }); } - public static String[] getRoles(Start start, AppIdentifier appIdentifier) throws SQLException, StorageQueryException { + public static String[] getRoles(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { String QUERY = "SELECT role FROM " + getConfig(start).getRolesTable() + " WHERE app_id = ?"; return execute(start, QUERY, pst -> pst.setString(1, appIdentifier.getAppId()), result -> { ArrayList roles = new ArrayList<>(); @@ -235,9 +224,9 @@ public static boolean deleteRoleForUser_Transaction(Start start, Connection con, return rowUpdatedCount > 0; } - public static boolean doesRoleExist_transaction(Start start, Connection con, AppIdentifier appIdentifier, String role) + public static boolean doesRoleExist_transaction(Start start, Connection con, AppIdentifier appIdentifier, + String role) throws SQLException, StorageQueryException { - ((ConnectionWithLocks) con).lock(appIdentifier.getAppId() + "~" + role + Config.getConfig(start).getRolesTable()); String QUERY = "SELECT 1 FROM " + getConfig(start).getRolesTable() @@ -248,7 +237,8 @@ public static boolean doesRoleExist_transaction(Start start, Connection con, App }, ResultSet::next); } - public static String[] getUsersForRole(Start start, TenantIdentifier tenantIdentifier, String role) throws SQLException, StorageQueryException { + public static String[] getUsersForRole(Start start, TenantIdentifier tenantIdentifier, String role) + throws SQLException, StorageQueryException { String QUERY = "SELECT user_id FROM " + getConfig(start).getUserRolesTable() + " WHERE app_id = ? AND tenant_id = ? AND role = ? "; return execute(start, QUERY, pst -> { @@ -266,7 +256,8 @@ public static String[] getUsersForRole(Start start, TenantIdentifier tenantIdent public static boolean deletePermissionForRole_Transaction(Start start, Connection con, AppIdentifier appIdentifier, String role, - String permission) throws SQLException, StorageQueryException { + String permission) + throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getUserRolesPermissionsTable() + " WHERE app_id = ? AND role = ? AND permission = ? "; @@ -314,7 +305,8 @@ public static String[] getRolesThatHavePermission(Start start, AppIdentifier app }); } - public static int deleteAllRolesForUser(Start start, TenantIdentifier tenantIdentifier, String userId) throws SQLException, StorageQueryException { + public static int deleteAllRolesForUser(Start start, TenantIdentifier tenantIdentifier, String userId) + throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getUserRolesTable() + " WHERE app_id = ? AND tenant_id = ? AND user_id = ?"; return update(start, QUERY, pst -> { @@ -324,10 +316,12 @@ public static int deleteAllRolesForUser(Start start, TenantIdentifier tenantIden }); } - public static int deleteAllRolesForUser(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { + public static int deleteAllRolesForUser_Transaction(Connection con, Start start, + AppIdentifier appIdentifier, String userId) + throws SQLException, StorageQueryException { String QUERY = "DELETE FROM " + getConfig(start).getUserRolesTable() + " WHERE app_id = ? AND user_id = ?"; - return update(start, QUERY, pst -> { + return update(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); pst.setString(2, userId); }); diff --git a/src/main/java/io/supertokens/multitenancy/Multitenancy.java b/src/main/java/io/supertokens/multitenancy/Multitenancy.java index 2140f984b..e97d3edf1 100644 --- a/src/main/java/io/supertokens/multitenancy/Multitenancy.java +++ b/src/main/java/io/supertokens/multitenancy/Multitenancy.java @@ -28,15 +28,20 @@ import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.exception.*; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateClientTypeException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateTenantException; import io.supertokens.pluginInterface.multitenancy.exceptions.DuplicateThirdPartyIdException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.multitenancy.sqlStorage.MultitenancySQLStorage; import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.storageLayer.StorageLayer; @@ -386,14 +391,135 @@ public static boolean addUserIdToTenant(Main main, TenantIdentifierWithStorage t String userId) throws TenantOrAppNotFoundException, UnknownUserIdException, StorageQueryException, FeatureNotEnabledException, DuplicateEmailException, DuplicatePhoneNumberException, - DuplicateThirdPartyUserException { + DuplicateThirdPartyUserException, AnotherPrimaryUserWithPhoneNumberAlreadyExistsException, + AnotherPrimaryUserWithEmailAlreadyExistsException, + AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException { if (Arrays.stream(FeatureFlag.getInstance(main, new AppIdentifier(null, null)).getEnabledFeatures()) .noneMatch(ee_features -> ee_features == EE_FEATURES.MULTI_TENANCY)) { throw new FeatureNotEnabledException(EE_FEATURES.MULTI_TENANCY); } - return tenantIdentifierWithStorage.getMultitenancyStorageWithTargetStorage() - .addUserIdToTenant(tenantIdentifierWithStorage, userId); + AuthRecipeSQLStorage storage = (AuthRecipeSQLStorage) tenantIdentifierWithStorage.getAuthRecipeStorage(); + try { + return storage.startTransaction(con -> { + String tenantId = tenantIdentifierWithStorage.getTenantId(); + AuthRecipeUserInfo userToAssociate = storage.getPrimaryUserById_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, userId); + + if (userToAssociate != null && userToAssociate.isPrimaryUser) { + Set emails = new HashSet<>(); + Set phoneNumbers = new HashSet<>(); + Set thirdParties = new HashSet<>(); + + // Loop through all the emails, phoneNumbers and thirdPartyInfos and check for conflicts + for (LoginMethod lM : userToAssociate.loginMethods) { + if (lM.email != null) { + emails.add(lM.email); + } + if (lM.phoneNumber != null) { + phoneNumbers.add(lM.phoneNumber); + } + if (lM.thirdParty != null) { + thirdParties.add(lM.thirdParty); + } + } + + for (String email : emails) { + AuthRecipeUserInfo[] usersWithSameEmail = storage.listPrimaryUsersByEmail_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, email); + for (AuthRecipeUserInfo userWithSameEmail : usersWithSameEmail) { + if (userWithSameEmail.getSupertokensUserId().equals(userToAssociate.getSupertokensUserId())) { + continue; // it's the same user, no need to check anything + } + if (userWithSameEmail.isPrimaryUser && userWithSameEmail.tenantIds.contains(tenantId) && !userWithSameEmail.getSupertokensUserId().equals(userId)) { + for (LoginMethod lm1 : userWithSameEmail.loginMethods) { + if (lm1.tenantIds.contains(tenantId)) { + for (LoginMethod lm2 : userToAssociate.loginMethods) { + if (lm1.recipeId.equals(lm2.recipeId) && email.equals(lm1.email) && lm1.email.equals(lm2.email)) { + throw new StorageTransactionLogicException(new DuplicateEmailException()); + } + } + } + } + throw new StorageTransactionLogicException(new AnotherPrimaryUserWithEmailAlreadyExistsException(userWithSameEmail.getSupertokensUserId())); + } + } + } + + for (String phoneNumber : phoneNumbers) { + AuthRecipeUserInfo[] usersWithSamePhoneNumber = storage.listPrimaryUsersByPhoneNumber_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, phoneNumber); + for (AuthRecipeUserInfo userWithSamePhoneNumber : usersWithSamePhoneNumber) { + if (userWithSamePhoneNumber.getSupertokensUserId().equals(userToAssociate.getSupertokensUserId())) { + continue; // it's the same user, no need to check anything + } + if (userWithSamePhoneNumber.tenantIds.contains(tenantId) && !userWithSamePhoneNumber.getSupertokensUserId().equals(userId)) { + for (LoginMethod lm1 : userWithSamePhoneNumber.loginMethods) { + if (lm1.tenantIds.contains(tenantId)) { + for (LoginMethod lm2 : userToAssociate.loginMethods) { + if (lm1.recipeId.equals(lm2.recipeId) && phoneNumber.equals(lm1.phoneNumber) && lm1.phoneNumber.equals(lm2.phoneNumber)) { + throw new StorageTransactionLogicException(new DuplicatePhoneNumberException()); + } + } + } + } + throw new StorageTransactionLogicException(new AnotherPrimaryUserWithPhoneNumberAlreadyExistsException(userWithSamePhoneNumber.getSupertokensUserId())); + } + } + } + + for (LoginMethod.ThirdParty tp : thirdParties) { + AuthRecipeUserInfo[] usersWithSameThirdPartyInfo = storage.listPrimaryUsersByThirdPartyInfo_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, tp.id, tp.userId); + for (AuthRecipeUserInfo userWithSameThirdPartyInfo : usersWithSameThirdPartyInfo) { + if (userWithSameThirdPartyInfo.getSupertokensUserId().equals(userToAssociate.getSupertokensUserId())) { + continue; // it's the same user, no need to check anything + } + if (userWithSameThirdPartyInfo.tenantIds.contains(tenantId) && !userWithSameThirdPartyInfo.getSupertokensUserId().equals(userId)) { + for (LoginMethod lm1 : userWithSameThirdPartyInfo.loginMethods) { + if (lm1.tenantIds.contains(tenantId)) { + for (LoginMethod lm2 : userToAssociate.loginMethods) { + if (lm1.recipeId.equals(lm2.recipeId) && tp.equals(lm1.thirdParty) && lm1.thirdParty.equals(lm2.thirdParty)) { + throw new StorageTransactionLogicException(new DuplicateThirdPartyUserException()); + } + } + } + } + + throw new StorageTransactionLogicException(new AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException(userWithSameThirdPartyInfo.getSupertokensUserId())); + } + } + } + } + + // userToAssociate may be null if the user is not associated to any tenants, we can still try and + // associate it. This happens only in CDI 3.0 where we allow disassociation from all tenants + // This will not happen in CDI >= 4.0 because we will not allow disassociation from all tenants + try { + boolean result = ((MultitenancySQLStorage) storage).addUserIdToTenant_Transaction(tenantIdentifierWithStorage, con, userId); + storage.commitTransaction(con); + return result; + } catch (TenantOrAppNotFoundException | UnknownUserIdException | DuplicatePhoneNumberException | + DuplicateThirdPartyUserException | DuplicateEmailException e) { + throw new StorageTransactionLogicException(e); + } + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof DuplicateEmailException) { + throw (DuplicateEmailException) e.actualException; + } else if (e.actualException instanceof DuplicatePhoneNumberException) { + throw (DuplicatePhoneNumberException) e.actualException; + } else if (e.actualException instanceof DuplicateThirdPartyUserException) { + throw (DuplicateThirdPartyUserException) e.actualException; + } else if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } else if (e.actualException instanceof UnknownUserIdException) { + throw (UnknownUserIdException) e.actualException; + } else if (e.actualException instanceof AnotherPrimaryUserWithPhoneNumberAlreadyExistsException) { + throw (AnotherPrimaryUserWithPhoneNumberAlreadyExistsException) e.actualException; + } else if (e.actualException instanceof AnotherPrimaryUserWithEmailAlreadyExistsException) { + throw (AnotherPrimaryUserWithEmailAlreadyExistsException) e.actualException; + } else if (e.actualException instanceof AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException) { + throw (AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException) e.actualException; + } + throw new StorageQueryException(e.actualException); + } } public static boolean removeUserIdFromTenant(Main main, TenantIdentifierWithStorage tenantIdentifierWithStorage, diff --git a/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithEmailAlreadyExistsException.java b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithEmailAlreadyExistsException.java new file mode 100644 index 000000000..c95bfdcc3 --- /dev/null +++ b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithEmailAlreadyExistsException.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.multitenancy.exception; + +public class AnotherPrimaryUserWithEmailAlreadyExistsException extends Exception { + public AnotherPrimaryUserWithEmailAlreadyExistsException(String primaryUserId) { + super("Another primary user with email already exists: " + primaryUserId); + } +} diff --git a/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithPhoneNumberAlreadyExistsException.java b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithPhoneNumberAlreadyExistsException.java new file mode 100644 index 000000000..e012f9349 --- /dev/null +++ b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithPhoneNumberAlreadyExistsException.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.multitenancy.exception; + +public class AnotherPrimaryUserWithPhoneNumberAlreadyExistsException extends Exception { + public AnotherPrimaryUserWithPhoneNumberAlreadyExistsException(String primaryUserId) { + super("Another primary user with phone number already exists: " + primaryUserId); + } +} diff --git a/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException.java b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException.java new file mode 100644 index 000000000..d5f413cf5 --- /dev/null +++ b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.multitenancy.exception; + +public class AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException extends Exception { + public AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException(String primaryUserId) { + super("Another primary user with third party info already exists: " + primaryUserId); + } +} diff --git a/src/main/java/io/supertokens/passwordless/Passwordless.java b/src/main/java/io/supertokens/passwordless/Passwordless.java index 29a38cd02..798d0b6ae 100644 --- a/src/main/java/io/supertokens/passwordless/Passwordless.java +++ b/src/main/java/io/supertokens/passwordless/Passwordless.java @@ -17,11 +17,17 @@ package io.supertokens.passwordless; import io.supertokens.Main; +import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.config.Config; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.passwordless.exceptions.*; +import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; @@ -33,7 +39,6 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; -import io.supertokens.pluginInterface.passwordless.UserInfo; import io.supertokens.pluginInterface.passwordless.exception.*; import io.supertokens.pluginInterface.passwordless.sqlStorage.PasswordlessSQLStorage; import io.supertokens.storageLayer.StorageLayer; @@ -46,6 +51,7 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Stream; @@ -72,7 +78,8 @@ public static CreateCodeResponse createCode(Main main, String email, String phon } } - public static CreateCodeResponse createCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String email, + public static CreateCodeResponse createCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, + String email, String phoneNumber, @Nullable String deviceId, @Nullable String userInputCode) throws RestartFlowException, DuplicateLinkCodeHashException, @@ -190,7 +197,8 @@ public static List getDevicesWithCodesByEmail( PasswordlessDevice[] devices = passwordlessStorage.getDevicesByEmail(tenantIdentifierWithStorage, email); ArrayList result = new ArrayList(); for (PasswordlessDevice device : devices) { - PasswordlessCode[] codes = passwordlessStorage.getCodesOfDevice(tenantIdentifierWithStorage, device.deviceIdHash); + PasswordlessCode[] codes = passwordlessStorage.getCodesOfDevice(tenantIdentifierWithStorage, + device.deviceIdHash); result.add(new DeviceWithCodes(device, codes)); } @@ -210,10 +218,12 @@ public static List getDevicesWithCodesByPhoneNumber( throws StorageQueryException { PasswordlessSQLStorage passwordlessStorage = tenantIdentifierWithStorage.getPasswordlessStorage(); - PasswordlessDevice[] devices = passwordlessStorage.getDevicesByPhoneNumber(tenantIdentifierWithStorage, phoneNumber); + PasswordlessDevice[] devices = passwordlessStorage.getDevicesByPhoneNumber(tenantIdentifierWithStorage, + phoneNumber); ArrayList result = new ArrayList(); for (PasswordlessDevice device : devices) { - PasswordlessCode[] codes = passwordlessStorage.getCodesOfDevice(tenantIdentifierWithStorage, device.deviceIdHash); + PasswordlessCode[] codes = passwordlessStorage.getCodesOfDevice(tenantIdentifierWithStorage, + device.deviceIdHash); result.add(new DeviceWithCodes(device, codes)); } @@ -240,12 +250,13 @@ public static ConsumeCodeResponse consumeCode(Main main, Storage storage = StorageLayer.getStorage(main); return consumeCode( new TenantIdentifierWithStorage(null, null, null, storage), - main, deviceId, deviceIdHashFromUser, userInputCode, linkCode); + main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, false); } catch (TenantOrAppNotFoundException | BadPermissionException e) { throw new IllegalStateException(e); } } + @TestOnly public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String deviceId, String deviceIdHashFromUser, String userInputCode, String linkCode) @@ -253,6 +264,17 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant IncorrectUserInputCodeException, DeviceIdHashMismatchException, StorageTransactionLogicException, StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException, TenantOrAppNotFoundException, BadPermissionException { + return consumeCode(tenantIdentifierWithStorage, main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, + false); + } + + public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, + String deviceId, String deviceIdHashFromUser, + String userInputCode, String linkCode, boolean setEmailVerified) + throws RestartFlowException, ExpiredUserInputCodeException, + IncorrectUserInputCodeException, DeviceIdHashMismatchException, StorageTransactionLogicException, + StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException, + TenantOrAppNotFoundException, BadPermissionException { TenantConfig config = Multitenancy.getTenantInfo(main, tenantIdentifierWithStorage); if (config == null) { @@ -274,7 +296,8 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant PasswordlessLinkCode parsedCode = PasswordlessLinkCode.decodeString(linkCode); linkCodeHash = parsedCode.getHash(); - PasswordlessCode code = passwordlessStorage.getCodeByLinkCodeHash(tenantIdentifierWithStorage, linkCodeHash.encode()); + PasswordlessCode code = passwordlessStorage.getCodeByLinkCodeHash(tenantIdentifierWithStorage, + linkCodeHash.encode()); if (code == null || code.createdAt < (System.currentTimeMillis() - passwordlessCodeLifetime)) { throw new RestartFlowException(); } @@ -284,7 +307,8 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant PasswordlessDeviceId parsedDeviceId = PasswordlessDeviceId.decodeString(deviceId); deviceIdHash = parsedDeviceId.getHash(); - PasswordlessDevice device = passwordlessStorage.getDevice(tenantIdentifierWithStorage, deviceIdHash.encode()); + PasswordlessDevice device = passwordlessStorage.getDevice(tenantIdentifierWithStorage, + deviceIdHash.encode()); if (device == null) { throw new RestartFlowException(); } @@ -306,12 +330,14 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant } if (device.failedAttempts >= maxCodeInputAttempts) { // This can happen if the configured maxCodeInputAttempts changes - passwordlessStorage.deleteDevice_Transaction(tenantIdentifierWithStorage, con, deviceIdHash.encode()); + passwordlessStorage.deleteDevice_Transaction(tenantIdentifierWithStorage, con, + deviceIdHash.encode()); passwordlessStorage.commitTransaction(con); throw new StorageTransactionLogicException(new RestartFlowException()); } - PasswordlessCode code = passwordlessStorage.getCodeByLinkCodeHash_Transaction(tenantIdentifierWithStorage, con, + PasswordlessCode code = passwordlessStorage.getCodeByLinkCodeHash_Transaction( + tenantIdentifierWithStorage, con, linkCodeHash.encode()); if (code == null || code.createdAt < System.currentTimeMillis() - passwordlessCodeLifetime) { if (deviceId != null) { @@ -319,11 +345,13 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant // the code expired. This means that we need to increment failedAttempts or clean up the device // if it would exceed the configured max. if (device.failedAttempts + 1 >= maxCodeInputAttempts) { - passwordlessStorage.deleteDevice_Transaction(tenantIdentifierWithStorage, con, deviceIdHash.encode()); + passwordlessStorage.deleteDevice_Transaction(tenantIdentifierWithStorage, con, + deviceIdHash.encode()); passwordlessStorage.commitTransaction(con); throw new StorageTransactionLogicException(new RestartFlowException()); } else { - passwordlessStorage.incrementDeviceFailedAttemptCount_Transaction(tenantIdentifierWithStorage, con, + passwordlessStorage.incrementDeviceFailedAttemptCount_Transaction( + tenantIdentifierWithStorage, con, deviceIdHash.encode()); passwordlessStorage.commitTransaction(con); @@ -340,7 +368,8 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant } if (device.email != null) { - passwordlessStorage.deleteDevicesByEmail_Transaction(tenantIdentifierWithStorage, con, device.email); + passwordlessStorage.deleteDevicesByEmail_Transaction(tenantIdentifierWithStorage, con, + device.email); } else if (device.phoneNumber != null) { passwordlessStorage.deleteDevicesByPhoneNumber_Transaction(tenantIdentifierWithStorage, con, device.phoneNumber); @@ -363,9 +392,35 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant } // Getting here means that we successfully consumed the code - UserInfo user = consumedDevice.email != null ? - passwordlessStorage.getUserByEmail(tenantIdentifierWithStorage, consumedDevice.email) - : passwordlessStorage.getUserByPhoneNumber(tenantIdentifierWithStorage, consumedDevice.phoneNumber); + AuthRecipeUserInfo user = null; + LoginMethod loginMethod = null; + if (consumedDevice.email != null) { + AuthRecipeUserInfo[] users = passwordlessStorage.listPrimaryUsersByEmail(tenantIdentifierWithStorage, + consumedDevice.email); + for (AuthRecipeUserInfo currUser : users) { + for (LoginMethod currLM : currUser.loginMethods) { + if (currLM.recipeId == RECIPE_ID.PASSWORDLESS && currLM.email.equals(consumedDevice.email) && currLM.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { + user = currUser; + loginMethod = currLM; + break; + } + } + } + } else { + AuthRecipeUserInfo[] users = passwordlessStorage.listPrimaryUsersByPhoneNumber(tenantIdentifierWithStorage, + consumedDevice.phoneNumber); + for (AuthRecipeUserInfo currUser : users) { + for (LoginMethod currLM : currUser.loginMethods) { + if (currLM.recipeId == RECIPE_ID.PASSWORDLESS && + currLM.phoneNumber.equals(consumedDevice.phoneNumber) && currLM.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { + user = currUser; + loginMethod = currLM; + break; + } + } + } + } + if (user == null) { while (true) { try { @@ -373,7 +428,34 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant long timeJoined = System.currentTimeMillis(); user = passwordlessStorage.createUser(tenantIdentifierWithStorage, userId, consumedDevice.email, consumedDevice.phoneNumber, timeJoined); - return new ConsumeCodeResponse(true, user); + + // Set email as verified, if using email + if (setEmailVerified && consumedDevice.email != null) { + try { + AuthRecipeUserInfo finalUser = user; + tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage() + .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + finalUser.getSupertokensUserId(), consumedDevice.email, true); + tenantIdentifierWithStorage.getEmailVerificationStorage() + .commitTransaction(con); + + return null; + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } + }); + user.loginMethods[0].setVerified(); // newly created user has only one loginMethod + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } + throw new StorageQueryException(e); + } + } + + return new ConsumeCodeResponse(true, user, consumedDevice.email, consumedDevice.phoneNumber); } catch (DuplicateEmailException | DuplicatePhoneNumberException e) { // Getting these would mean that between getting the user and trying creating it: // 1. the user managed to do a full create+consume flow @@ -389,14 +471,40 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant } else { // We do not need this cleanup if we are creating the user, since it uses the email/phoneNumber of the // device, which has already been cleaned up - if (user.email != null && !user.email.equals(consumedDevice.email)) { - removeCodesByEmail(tenantIdentifierWithStorage, user.email); + if (setEmailVerified && consumedDevice.email != null) { + // Set email verification + try { + LoginMethod finalLoginMethod = loginMethod; + tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage() + .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + finalLoginMethod.getSupertokensUserId(), consumedDevice.email, true); + tenantIdentifierWithStorage.getEmailVerificationStorage() + .commitTransaction(con); + + return null; + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } + }); + loginMethod.setVerified(); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } + throw new StorageQueryException(e); + } + } + + if (loginMethod.email != null && !loginMethod.email.equals(consumedDevice.email)) { + removeCodesByEmail(tenantIdentifierWithStorage, loginMethod.email); } - if (user.phoneNumber != null && !user.phoneNumber.equals(consumedDevice.phoneNumber)) { - removeCodesByPhoneNumber(tenantIdentifierWithStorage, user.phoneNumber); + if (loginMethod.phoneNumber != null && !loginMethod.phoneNumber.equals(consumedDevice.phoneNumber)) { + removeCodesByPhoneNumber(tenantIdentifierWithStorage, loginMethod.phoneNumber); } } - return new ConsumeCodeResponse(false, user); + return new ConsumeCodeResponse(false, user, consumedDevice.email, consumedDevice.phoneNumber); } @TestOnly @@ -421,7 +529,8 @@ public static void removeCode(TenantIdentifierWithStorage tenantIdentifierWithSt // Locking the device passwordlessStorage.getDevice_Transaction(tenantIdentifierWithStorage, con, code.deviceIdHash); - PasswordlessCode[] allCodes = passwordlessStorage.getCodesOfDevice_Transaction(tenantIdentifierWithStorage, con, + PasswordlessCode[] allCodes = passwordlessStorage.getCodesOfDevice_Transaction(tenantIdentifierWithStorage, + con, code.deviceIdHash); if (!Stream.of(allCodes).anyMatch(code::equals)) { // Already deleted @@ -482,107 +591,189 @@ public static void removeCodesByPhoneNumber(TenantIdentifierWithStorage tenantId } @TestOnly - public static UserInfo getUserById(Main main, String userId) + @Deprecated + public static AuthRecipeUserInfo getUserById(Main main, String userId) throws StorageQueryException { Storage storage = StorageLayer.getStorage(main); return getUserById( new AppIdentifierWithStorage(null, null, storage), userId); } - public static UserInfo getUserById(AppIdentifierWithStorage appIdentifierWithStorage, String userId) + @Deprecated + public static AuthRecipeUserInfo getUserById(AppIdentifierWithStorage appIdentifierWithStorage, String userId) throws StorageQueryException { - return appIdentifierWithStorage.getPasswordlessStorage() - .getUserById(appIdentifierWithStorage, userId); + AuthRecipeUserInfo result = appIdentifierWithStorage.getAuthRecipeStorage() + .getPrimaryUserById(appIdentifierWithStorage, userId); + if (result == null) { + return null; + } + for (LoginMethod lM : result.loginMethods) { + if (lM.getSupertokensUserId().equals(userId) && lM.recipeId == RECIPE_ID.PASSWORDLESS) { + return AuthRecipeUserInfo.create(lM.getSupertokensUserId(), result.isPrimaryUser, + lM); + } + } + return null; } @TestOnly - public static UserInfo getUserByPhoneNumber(Main main, - String phoneNumber) throws StorageQueryException { + public static AuthRecipeUserInfo getUserByPhoneNumber(Main main, + String phoneNumber) throws StorageQueryException { Storage storage = StorageLayer.getStorage(main); return getUserByPhoneNumber( new TenantIdentifierWithStorage(null, null, null, storage), phoneNumber); } - public static UserInfo getUserByPhoneNumber(TenantIdentifierWithStorage tenantIdentifierWithStorage, - String phoneNumber) throws StorageQueryException { - return tenantIdentifierWithStorage.getPasswordlessStorage() - .getUserByPhoneNumber(tenantIdentifierWithStorage, phoneNumber); + @Deprecated + public static AuthRecipeUserInfo getUserByPhoneNumber(TenantIdentifierWithStorage tenantIdentifierWithStorage, + String phoneNumber) throws StorageQueryException { + AuthRecipeUserInfo[] users = tenantIdentifierWithStorage.getPasswordlessStorage() + .listPrimaryUsersByPhoneNumber(tenantIdentifierWithStorage, phoneNumber); + for (AuthRecipeUserInfo user : users) { + for (LoginMethod lM : user.loginMethods) { + if (lM.recipeId == RECIPE_ID.PASSWORDLESS && lM.phoneNumber.equals(phoneNumber)) { + return user; + } + } + } + return null; } + @Deprecated @TestOnly - public static UserInfo getUserByEmail(Main main, String email) + public static AuthRecipeUserInfo getUserByEmail(Main main, String email) throws StorageQueryException { Storage storage = StorageLayer.getStorage(main); return getUserByEmail( - new TenantIdentifierWithStorage(null, null, null,storage), email); + new TenantIdentifierWithStorage(null, null, null, storage), email); } - public static UserInfo getUserByEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage, String email) + @Deprecated + public static AuthRecipeUserInfo getUserByEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage, + String email) throws StorageQueryException { - return tenantIdentifierWithStorage.getPasswordlessStorage().getUserByEmail(tenantIdentifierWithStorage, email); + AuthRecipeUserInfo[] users = tenantIdentifierWithStorage.getPasswordlessStorage() + .listPrimaryUsersByEmail(tenantIdentifierWithStorage, email); + for (AuthRecipeUserInfo user : users) { + for (LoginMethod lM : user.loginMethods) { + if (lM.recipeId == RECIPE_ID.PASSWORDLESS && lM.email.equals(email)) { + return user; + } + } + } + return null; } @TestOnly public static void updateUser(Main main, String userId, FieldUpdate emailUpdate, FieldUpdate phoneNumberUpdate) throws StorageQueryException, UnknownUserIdException, DuplicateEmailException, - DuplicatePhoneNumberException, UserWithoutContactInfoException { + DuplicatePhoneNumberException, UserWithoutContactInfoException, EmailChangeNotAllowedException, + PhoneNumberChangeNotAllowedException { Storage storage = StorageLayer.getStorage(main); updateUser(new AppIdentifierWithStorage(null, null, storage), userId, emailUpdate, phoneNumberUpdate); } - public static void updateUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId, + public static void updateUser(AppIdentifierWithStorage appIdentifierWithStorage, String recipeUserId, FieldUpdate emailUpdate, FieldUpdate phoneNumberUpdate) throws StorageQueryException, UnknownUserIdException, DuplicateEmailException, - DuplicatePhoneNumberException, UserWithoutContactInfoException { + DuplicatePhoneNumberException, UserWithoutContactInfoException, EmailChangeNotAllowedException, + PhoneNumberChangeNotAllowedException { PasswordlessSQLStorage storage = appIdentifierWithStorage.getPasswordlessStorage(); // We do not lock the user here, because we decided that even if the device cleanup used outdated information // it wouldn't leave the system in an incosistent state/cause problems. - UserInfo user = storage.getUserById(appIdentifierWithStorage, userId); + AuthRecipeUserInfo user = AuthRecipe.getUserById(appIdentifierWithStorage, recipeUserId); if (user == null) { throw new UnknownUserIdException(); } - boolean emailWillBeDefined = emailUpdate != null ? emailUpdate.newValue != null : user.email != null; + + LoginMethod lM = Arrays.stream(user.loginMethods) + .filter(currlM -> currlM.getSupertokensUserId().equals(recipeUserId) && currlM.recipeId == RECIPE_ID.PASSWORDLESS) + .findFirst().orElse(null); + + if (lM == null) { + throw new UnknownUserIdException(); + } + + boolean emailWillBeDefined = emailUpdate != null ? emailUpdate.newValue != null : lM.email != null; boolean phoneNumberWillBeDefined = phoneNumberUpdate != null ? phoneNumberUpdate.newValue != null - : user.phoneNumber != null; + : lM.phoneNumber != null; if (!emailWillBeDefined && !phoneNumberWillBeDefined) { throw new UserWithoutContactInfoException(); } try { + AuthRecipeSQLStorage authRecipeSQLStorage = + (AuthRecipeSQLStorage) appIdentifierWithStorage.getAuthRecipeStorage(); storage.startTransaction(con -> { - if (emailUpdate != null && !Objects.equals(emailUpdate.newValue, user.email)) { + if (emailUpdate != null && !Objects.equals(emailUpdate.newValue, lM.email)) { + if (user.isPrimaryUser) { + for (String tenantId : user.tenantIds) { + AuthRecipeUserInfo[] existingUsersWithNewEmail = + authRecipeSQLStorage.listPrimaryUsersByEmail_Transaction( + appIdentifierWithStorage, con, + emailUpdate.newValue); + + for (AuthRecipeUserInfo userWithSameEmail : existingUsersWithNewEmail) { + if (!userWithSameEmail.tenantIds.contains(tenantId)) { + continue; + } + if (userWithSameEmail.isPrimaryUser && !userWithSameEmail.getSupertokensUserId().equals(user.getSupertokensUserId())) { + throw new StorageTransactionLogicException( + new EmailChangeNotAllowedException()); + } + } + } + } try { - storage.updateUserEmail_Transaction(appIdentifierWithStorage, con, userId, + storage.updateUserEmail_Transaction(appIdentifierWithStorage, con, recipeUserId, emailUpdate.newValue); } catch (UnknownUserIdException | DuplicateEmailException e) { throw new StorageTransactionLogicException(e); } - if (user.email != null) { - storage.deleteDevicesByEmail_Transaction(appIdentifierWithStorage, con, user.email, - userId); + if (lM.email != null) { + storage.deleteDevicesByEmail_Transaction(appIdentifierWithStorage, con, lM.email, + recipeUserId); } if (emailUpdate.newValue != null) { storage.deleteDevicesByEmail_Transaction(appIdentifierWithStorage, con, - emailUpdate.newValue, userId); + emailUpdate.newValue, recipeUserId); } } - if (phoneNumberUpdate != null && !Objects.equals(phoneNumberUpdate.newValue, user.phoneNumber)) { + if (phoneNumberUpdate != null && !Objects.equals(phoneNumberUpdate.newValue, lM.phoneNumber)) { + if (user.isPrimaryUser) { + for (String tenantId : user.tenantIds) { + AuthRecipeUserInfo[] existingUsersWithNewPhoneNumber = + authRecipeSQLStorage.listPrimaryUsersByPhoneNumber_Transaction( + appIdentifierWithStorage, con, + phoneNumberUpdate.newValue); + + for (AuthRecipeUserInfo userWithSamePhoneNumber : existingUsersWithNewPhoneNumber) { + if (!userWithSamePhoneNumber.tenantIds.contains(tenantId)) { + continue; + } + if (userWithSamePhoneNumber.isPrimaryUser && !userWithSamePhoneNumber.getSupertokensUserId().equals(user.getSupertokensUserId())) { + throw new StorageTransactionLogicException( + new PhoneNumberChangeNotAllowedException()); + } + } + } + } try { - storage.updateUserPhoneNumber_Transaction(appIdentifierWithStorage, con, userId, + storage.updateUserPhoneNumber_Transaction(appIdentifierWithStorage, con, recipeUserId, phoneNumberUpdate.newValue); } catch (UnknownUserIdException | DuplicatePhoneNumberException e) { throw new StorageTransactionLogicException(e); } - if (user.phoneNumber != null) { + if (lM.phoneNumber != null) { storage.deleteDevicesByPhoneNumber_Transaction(appIdentifierWithStorage, con, - user.phoneNumber, userId); + lM.phoneNumber, recipeUserId); } if (phoneNumberUpdate.newValue != null) { storage.deleteDevicesByPhoneNumber_Transaction(appIdentifierWithStorage, con, - phoneNumberUpdate.newValue, userId); + phoneNumberUpdate.newValue, recipeUserId); } } storage.commitTransaction(con); @@ -600,6 +791,14 @@ public static void updateUser(AppIdentifierWithStorage appIdentifierWithStorage, if (e.actualException instanceof DuplicatePhoneNumberException) { throw (DuplicatePhoneNumberException) e.actualException; } + + if (e.actualException instanceof EmailChangeNotAllowedException) { + throw (EmailChangeNotAllowedException) e.actualException; + } + + if (e.actualException instanceof PhoneNumberChangeNotAllowedException) { + throw (PhoneNumberChangeNotAllowedException) e.actualException; + } } } @@ -646,11 +845,15 @@ public CreateCodeResponse(String deviceIdHash, String codeId, String deviceId, S public static class ConsumeCodeResponse { public boolean createdNewUser; - public UserInfo user; + public AuthRecipeUserInfo user; + public String email; + public String phoneNumber; - public ConsumeCodeResponse(boolean createdNewUser, UserInfo user) { + public ConsumeCodeResponse(boolean createdNewUser, AuthRecipeUserInfo user, String email, String phoneNumber) { this.createdNewUser = createdNewUser; this.user = user; + this.email = email; + this.phoneNumber = phoneNumber; } } diff --git a/src/main/java/io/supertokens/passwordless/exceptions/PhoneNumberChangeNotAllowedException.java b/src/main/java/io/supertokens/passwordless/exceptions/PhoneNumberChangeNotAllowedException.java new file mode 100644 index 000000000..d0117d1cc --- /dev/null +++ b/src/main/java/io/supertokens/passwordless/exceptions/PhoneNumberChangeNotAllowedException.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.passwordless.exceptions; + +public class PhoneNumberChangeNotAllowedException extends Exception { +} diff --git a/src/main/java/io/supertokens/session/Session.java b/src/main/java/io/supertokens/session/Session.java index 51771bb21..dcbbdd204 100644 --- a/src/main/java/io/supertokens/session/Session.java +++ b/src/main/java/io/supertokens/session/Session.java @@ -29,6 +29,8 @@ import io.supertokens.multitenancy.Multitenancy; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.*; @@ -64,7 +66,7 @@ public class Session { @TestOnly public static SessionInformationHolder createNewSession(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, - @Nonnull String userId, + @Nonnull String recipeUserId, @Nonnull JsonObject userDataInJWT, @Nonnull JsonObject userDataInDatabase) throws NoSuchAlgorithmException, StorageQueryException, InvalidKeyException, @@ -72,7 +74,8 @@ public static SessionInformationHolder createNewSession(TenantIdentifierWithStor BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException, UnauthorisedException, JWT.JWTException, UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError { try { - return createNewSession(tenantIdentifierWithStorage, main, userId, userDataInJWT, userDataInDatabase, false, + return createNewSession(tenantIdentifierWithStorage, main, recipeUserId, userDataInJWT, userDataInDatabase, + false, AccessToken.getLatestVersion(), false); } catch (TenantOrAppNotFoundException e) { throw new IllegalStateException(e); @@ -81,7 +84,7 @@ public static SessionInformationHolder createNewSession(TenantIdentifierWithStor @TestOnly public static SessionInformationHolder createNewSession(Main main, - @Nonnull String userId, + @Nonnull String recipeUserId, @Nonnull JsonObject userDataInJWT, @Nonnull JsonObject userDataInDatabase) throws NoSuchAlgorithmException, StorageQueryException, InvalidKeyException, @@ -92,14 +95,14 @@ public static SessionInformationHolder createNewSession(Main main, try { return createNewSession( new TenantIdentifierWithStorage(null, null, null, storage), main, - userId, userDataInJWT, userDataInDatabase, false, AccessToken.getLatestVersion(), false); + recipeUserId, userDataInJWT, userDataInDatabase, false, AccessToken.getLatestVersion(), false); } catch (TenantOrAppNotFoundException e) { throw new IllegalStateException(e); } } @TestOnly - public static SessionInformationHolder createNewSession(Main main, @Nonnull String userId, + public static SessionInformationHolder createNewSession(Main main, @Nonnull String recipeUserId, @Nonnull JsonObject userDataInJWT, @Nonnull JsonObject userDataInDatabase, boolean enableAntiCsrf, AccessToken.VERSION version, @@ -112,14 +115,14 @@ public static SessionInformationHolder createNewSession(Main main, @Nonnull Stri try { return createNewSession( new TenantIdentifierWithStorage(null, null, null, storage), main, - userId, userDataInJWT, userDataInDatabase, enableAntiCsrf, version, useStaticKey); + recipeUserId, userDataInJWT, userDataInDatabase, enableAntiCsrf, version, useStaticKey); } catch (TenantOrAppNotFoundException e) { throw new IllegalStateException(e); } } public static SessionInformationHolder createNewSession(TenantIdentifierWithStorage tenantIdentifierWithStorage, - Main main, @Nonnull String userId, + Main main, @Nonnull String recipeUserId, @Nonnull JsonObject userDataInJWT, @Nonnull JsonObject userDataInDatabase, boolean enableAntiCsrf, AccessToken.VERSION version, @@ -133,23 +136,35 @@ public static SessionInformationHolder createNewSession(TenantIdentifierWithStor sessionHandle += "_" + tenantIdentifierWithStorage.getTenantId(); } + String primaryUserId = recipeUserId; + if (tenantIdentifierWithStorage.getStorage().getType().equals(STORAGE_TYPE.SQL)) { + tenantIdentifierWithStorage.getAuthRecipeStorage() + .getPrimaryUserIdStrForUserId(tenantIdentifierWithStorage.toAppIdentifier(), recipeUserId); + if (primaryUserId == null) { + primaryUserId = recipeUserId; + } + } + String antiCsrfToken = enableAntiCsrf ? UUID.randomUUID().toString() : null; final TokenInfo refreshToken = RefreshToken.createNewRefreshToken(tenantIdentifierWithStorage, main, - sessionHandle, userId, null, + sessionHandle, recipeUserId, null, antiCsrfToken); TokenInfo accessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage, main, sessionHandle, - userId, Utils.hashSHA256(refreshToken.token), null, userDataInJWT, antiCsrfToken, + recipeUserId, primaryUserId, Utils.hashSHA256(refreshToken.token), null, userDataInJWT, antiCsrfToken, null, version, useStaticKey); tenantIdentifierWithStorage.getSessionStorage() - .createNewSession(tenantIdentifierWithStorage, sessionHandle, userId, + .createNewSession(tenantIdentifierWithStorage, sessionHandle, recipeUserId, Utils.hashSHA256(Utils.hashSHA256(refreshToken.token)), userDataInDatabase, refreshToken.expiry, userDataInJWT, refreshToken.createdTime, useStaticKey); TokenInfo idRefreshToken = new TokenInfo(UUID.randomUUID().toString(), refreshToken.expiry, refreshToken.createdTime); - return new SessionInformationHolder(new SessionInfo(sessionHandle, userId, userDataInJWT, tenantIdentifierWithStorage.getTenantId()), accessToken, + return new SessionInformationHolder( + new SessionInfo(sessionHandle, primaryUserId, recipeUserId, userDataInJWT, + tenantIdentifierWithStorage.getTenantId()), + accessToken, refreshToken, idRefreshToken, antiCsrfToken); } @@ -201,7 +216,8 @@ public static SessionInformationHolder regenerateToken(AppIdentifier appIdentifi accessToken.sessionHandle); JsonObject newJWTUserPayload = userDataInJWT == null ? sessionInfo.userDataInJWT : userDataInJWT; - updateSession(tenantIdentifierWithStorage, accessToken.sessionHandle, null, newJWTUserPayload, accessToken.version); + updateSession(tenantIdentifierWithStorage, accessToken.sessionHandle, null, newJWTUserPayload, + accessToken.version); // if the above succeeds but the below fails, it's OK since the client will get server error and will try // again. In this case, the JWT data will be updated again since the API will get the old JWT. In case there @@ -210,17 +226,21 @@ public static SessionInformationHolder regenerateToken(AppIdentifier appIdentifi // in this case, we set the should not set the access token in the response since they will have to call // the refresh API anyway. return new SessionInformationHolder( - new SessionInfo(accessToken.sessionHandle, accessToken.userId, newJWTUserPayload, tenantIdentifierWithStorage.getTenantId()), null, null, null, + new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, accessToken.recipeUserId, + newJWTUserPayload, + tenantIdentifierWithStorage.getTenantId()), null, null, null, null); } TokenInfo newAccessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage, main, - accessToken.sessionHandle, accessToken.userId, + accessToken.sessionHandle, accessToken.recipeUserId, accessToken.primaryUserId, accessToken.refreshTokenHash1, accessToken.parentRefreshTokenHash1, newJWTUserPayload, accessToken.antiCsrfToken, accessToken.expiryTime, accessToken.version, sessionInfo.useStaticKey); return new SessionInformationHolder( - new SessionInfo(accessToken.sessionHandle, accessToken.userId, newJWTUserPayload, tenantIdentifierWithStorage.getTenantId()), + new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, accessToken.recipeUserId, + newJWTUserPayload, + tenantIdentifierWithStorage.getTenantId()), new TokenInfo(newAccessToken.token, newAccessToken.expiry, newAccessToken.createdTime), null, null, null); } @@ -255,18 +275,22 @@ public static SessionInformationHolder regenerateTokenBeforeCDI2_21(AppIdentifie // in this case, we set the should not set the access token in the response since they will have to call // the refresh API anyway. return new SessionInformationHolder( - new SessionInfo(accessToken.sessionHandle, accessToken.userId, newJWTUserPayload, tenantIdentifierWithStorage.getTenantId()), null, null, null, + new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, accessToken.recipeUserId, + newJWTUserPayload, + tenantIdentifierWithStorage.getTenantId()), null, null, null, null); } TokenInfo newAccessToken = AccessToken.createNewAccessToken(accessToken.tenantIdentifier, main, accessToken.sessionHandle, - accessToken.userId, + accessToken.recipeUserId, accessToken.primaryUserId, accessToken.refreshTokenHash1, accessToken.parentRefreshTokenHash1, newJWTUserPayload, accessToken.antiCsrfToken, accessToken.expiryTime, accessToken.version, sessionInfo.useStaticKey); return new SessionInformationHolder( - new SessionInfo(accessToken.sessionHandle, accessToken.userId, newJWTUserPayload, tenantIdentifierWithStorage.getTenantId()), + new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, accessToken.recipeUserId, + newJWTUserPayload, + tenantIdentifierWithStorage.getTenantId()), new TokenInfo(newAccessToken.token, newAccessToken.expiry, newAccessToken.createdTime), null, null, null); } @@ -319,7 +343,9 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M // this means that the refresh token associated with this access token is // already the parent - and JWT payload doesn't need to be updated. return new SessionInformationHolder( - new SessionInfo(accessToken.sessionHandle, accessToken.userId, accessToken.userData, tenantIdentifierWithStorage.getTenantId()), null, null, + new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, accessToken.recipeUserId, + accessToken.userData, + tenantIdentifierWithStorage.getTenantId()), null, null, null, null); } @@ -360,18 +386,20 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M newAccessToken = AccessToken.createNewAccessTokenV1(tenantIdentifierWithStorage, main, accessToken.sessionHandle, - accessToken.userId, accessToken.refreshTokenHash1, null, + accessToken.recipeUserId, accessToken.refreshTokenHash1, null, sessionInfo.userDataInJWT, accessToken.antiCsrfToken); } else { newAccessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage, main, accessToken.sessionHandle, - accessToken.userId, accessToken.refreshTokenHash1, null, + accessToken.recipeUserId, accessToken.primaryUserId, + accessToken.refreshTokenHash1, null, sessionInfo.userDataInJWT, accessToken.antiCsrfToken, null, accessToken.version, sessionInfo.useStaticKey); } return new SessionInformationHolder( - new SessionInfo(accessToken.sessionHandle, accessToken.userId, + new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, + accessToken.recipeUserId, sessionInfo.userDataInJWT, tenantIdentifierWithStorage.getTenantId()), new TokenInfo(newAccessToken.token, newAccessToken.expiry, newAccessToken.createdTime), @@ -380,13 +408,16 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M storage.commitTransaction(con); return new SessionInformationHolder( - new SessionInfo(accessToken.sessionHandle, accessToken.userId, accessToken.userData, tenantIdentifierWithStorage.getTenantId()), + new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, + accessToken.recipeUserId, accessToken.userData, + tenantIdentifierWithStorage.getTenantId()), // here we purposely use accessToken.userData instead of sessionInfo.userDataInJWT // because we are not returning a new access token null, null, null, null); } catch (UnauthorisedException | NoSuchAlgorithmException | - InvalidKeyException | InvalidKeySpecException | SignatureException | - UnsupportedJWTSigningAlgorithmException | AccessTokenPayloadError | TenantOrAppNotFoundException e) { + InvalidKeyException | InvalidKeySpecException | SignatureException | + UnsupportedJWTSigningAlgorithmException | AccessTokenPayloadError | + TenantOrAppNotFoundException e) { throw new StorageTransactionLogicException(e); } }, SQLStorage.TransactionIsolationLevel.REPEATABLE_READ); @@ -433,29 +464,34 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M if (accessToken.version == AccessToken.VERSION.V1) { newAccessToken = AccessToken.createNewAccessTokenV1(tenantIdentifierWithStorage, main, accessToken.sessionHandle, - accessToken.userId, accessToken.refreshTokenHash1, null, sessionInfo.userDataInJWT, + accessToken.recipeUserId, accessToken.refreshTokenHash1, null, + sessionInfo.userDataInJWT, accessToken.antiCsrfToken); } else { newAccessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage, main, accessToken.sessionHandle, - accessToken.userId, accessToken.refreshTokenHash1, null, sessionInfo.userDataInJWT, + accessToken.recipeUserId, accessToken.primaryUserId, accessToken.refreshTokenHash1, + null, sessionInfo.userDataInJWT, accessToken.antiCsrfToken, null, accessToken.version, sessionInfo.useStaticKey); } return new SessionInformationHolder( - new SessionInfo(accessToken.sessionHandle, accessToken.userId, + new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, + accessToken.recipeUserId, sessionInfo.userDataInJWT, tenantIdentifierWithStorage.getTenantId()), new TokenInfo(newAccessToken.token, newAccessToken.expiry, newAccessToken.createdTime), null, null, null); } return new SessionInformationHolder( - new SessionInfo(accessToken.sessionHandle, accessToken.userId, accessToken.userData, tenantIdentifierWithStorage.getTenantId()), + new SessionInfo(accessToken.sessionHandle, accessToken.primaryUserId, + accessToken.recipeUserId, accessToken.userData, + tenantIdentifierWithStorage.getTenantId()), // here we purposely use accessToken.userData instead of sessionInfo.userDataInJWT // because we are not returning a new access token null, null, null, null); } catch (NoSuchAlgorithmException | InvalidKeyException - | InvalidKeySpecException | SignatureException e) { + | InvalidKeySpecException | SignatureException e) { throw new StorageTransactionLogicException(e); } } @@ -533,14 +569,16 @@ private static SessionInformationHolder refreshSessionHelper( if (sessionInfo.refreshTokenHash2.equals(Utils.hashSHA256(Utils.hashSHA256(refreshToken)))) { // at this point, the input refresh token is the parent one. storage.commitTransaction(con); + String antiCsrfToken = enableAntiCsrf ? UUID.randomUUID().toString() : null; final TokenInfo newRefreshToken = RefreshToken.createNewRefreshToken( tenantIdentifierWithStorage, main, sessionHandle, - sessionInfo.userId, Utils.hashSHA256(refreshToken), antiCsrfToken); + sessionInfo.recipeUserId, Utils.hashSHA256(refreshToken), antiCsrfToken); TokenInfo newAccessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage, main, sessionHandle, - sessionInfo.userId, Utils.hashSHA256(newRefreshToken.token), + sessionInfo.recipeUserId, sessionInfo.userId, + Utils.hashSHA256(newRefreshToken.token), Utils.hashSHA256(refreshToken), sessionInfo.userDataInJWT, antiCsrfToken, null, accessTokenVersion, sessionInfo.useStaticKey); @@ -548,7 +586,9 @@ private static SessionInformationHolder refreshSessionHelper( newRefreshToken.expiry, newRefreshToken.createdTime); return new SessionInformationHolder( - new SessionInfo(sessionHandle, sessionInfo.userId, sessionInfo.userDataInJWT, tenantIdentifierWithStorage.getTenantId()), + new SessionInfo(sessionHandle, sessionInfo.userId, sessionInfo.recipeUserId, + sessionInfo.userDataInJWT, + tenantIdentifierWithStorage.getTenantId()), newAccessToken, newRefreshToken, idRefreshToken, antiCsrfToken); } @@ -571,13 +611,15 @@ private static SessionInformationHolder refreshSessionHelper( storage.commitTransaction(con); - throw new TokenTheftDetectedException(sessionHandle, sessionInfo.userId); + throw new TokenTheftDetectedException(sessionHandle, sessionInfo.recipeUserId, + sessionInfo.userId); } catch (UnauthorisedException | NoSuchAlgorithmException | InvalidKeyException - | AccessTokenPayloadError | TokenTheftDetectedException | InvalidKeySpecException - | SignatureException | NoSuchPaddingException | InvalidAlgorithmParameterException - | IllegalBlockSizeException | BadPaddingException | UnsupportedJWTSigningAlgorithmException | - TenantOrAppNotFoundException e) { + | AccessTokenPayloadError | TokenTheftDetectedException | InvalidKeySpecException + | SignatureException | NoSuchPaddingException | InvalidAlgorithmParameterException + | IllegalBlockSizeException | BadPaddingException | + UnsupportedJWTSigningAlgorithmException | + TenantOrAppNotFoundException e) { throw new StorageTransactionLogicException(e); } }); @@ -619,10 +661,10 @@ private static SessionInformationHolder refreshSessionHelper( final TokenInfo newRefreshToken = RefreshToken.createNewRefreshToken( tenantIdentifierWithStorage, main, sessionHandle, - sessionInfo.userId, Utils.hashSHA256(refreshToken), antiCsrfToken); + sessionInfo.recipeUserId, Utils.hashSHA256(refreshToken), antiCsrfToken); TokenInfo newAccessToken = AccessToken.createNewAccessToken(tenantIdentifierWithStorage, main, sessionHandle, - sessionInfo.userId, Utils.hashSHA256(newRefreshToken.token), + sessionInfo.recipeUserId, sessionInfo.userId, Utils.hashSHA256(newRefreshToken.token), Utils.hashSHA256(refreshToken), sessionInfo.userDataInJWT, antiCsrfToken, null, accessTokenVersion, sessionInfo.useStaticKey); @@ -630,7 +672,9 @@ private static SessionInformationHolder refreshSessionHelper( newRefreshToken.createdTime); return new SessionInformationHolder( - new SessionInfo(sessionHandle, sessionInfo.userId, sessionInfo.userDataInJWT, tenantIdentifierWithStorage.getTenantId()), + new SessionInfo(sessionHandle, sessionInfo.userId, sessionInfo.recipeUserId, + sessionInfo.userDataInJWT, + tenantIdentifierWithStorage.getTenantId()), newAccessToken, newRefreshToken, idRefreshToken, antiCsrfToken); } @@ -653,11 +697,11 @@ private static SessionInformationHolder refreshSessionHelper( accessTokenVersion); } - throw new TokenTheftDetectedException(sessionHandle, sessionInfo.userId); + throw new TokenTheftDetectedException(sessionHandle, sessionInfo.recipeUserId, sessionInfo.userId); } catch (NoSuchAlgorithmException | InvalidKeyException - | InvalidKeySpecException | SignatureException | NoSuchPaddingException - | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { + | InvalidKeySpecException | SignatureException | NoSuchPaddingException + | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { throw new StorageTransactionLogicException(e); } } @@ -677,7 +721,8 @@ public static String[] revokeSessionUsingSessionHandles(Main main, sessionHandles); } - public static String[] revokeSessionUsingSessionHandles(Main main, AppIdentifierWithStorage appIdentifierWithStorage, + public static String[] revokeSessionUsingSessionHandles(Main main, + AppIdentifierWithStorage appIdentifierWithStorage, String[] sessionHandles) throws StorageQueryException { @@ -700,7 +745,8 @@ public static String[] revokeSessionUsingSessionHandles(Main main, AppIdentifier for (String tenantId : sessionHandleMap.keySet()) { String[] sessionHandlesForTenant = sessionHandleMap.get(tenantId).toArray(new String[0]); - TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifierWithStorage.getConnectionUriDomain(), appIdentifierWithStorage.getAppId(), tenantId); + TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifierWithStorage.getConnectionUriDomain(), + appIdentifierWithStorage.getAppId(), tenantId); TenantIdentifierWithStorage tenantIdentifierWithStorage = null; try { tenantIdentifierWithStorage = tenantIdentifier.withStorage( @@ -719,13 +765,14 @@ public static String[] revokeSessionUsingSessionHandles(Main main, AppIdentifier } private static String[] revokeSessionUsingSessionHandles(TenantIdentifierWithStorage tenantIdentifierWithStorage, - String[] sessionHandles) + String[] sessionHandles) throws StorageQueryException { Set validHandles = new HashSet<>(); if (sessionHandles.length > 1) { // we need to identify which sessionHandles are valid if there are more than one sessionHandles to revoke - // if there is only one sessionHandle to revoke, we would know if it was valid by the number of revoked sessions + // if there is only one sessionHandle to revoke, we would know if it was valid by the number of revoked + // sessions for (String sessionHandle : sessionHandles) { if (tenantIdentifierWithStorage.getSessionStorage() .getSession(tenantIdentifierWithStorage, sessionHandle) != null) { @@ -761,19 +808,24 @@ private static String[] revokeSessionUsingSessionHandles(TenantIdentifierWithSto public static String[] revokeAllSessionsForUser(Main main, String userId) throws StorageQueryException { Storage storage = StorageLayer.getStorage(main); return revokeAllSessionsForUser(main, - new AppIdentifierWithStorage(null, null, storage), userId); + new AppIdentifierWithStorage(null, null, storage), userId, true); } public static String[] revokeAllSessionsForUser(Main main, AppIdentifierWithStorage appIdentifierWithStorage, - String userId) throws StorageQueryException { - String[] sessionHandles = getAllNonExpiredSessionHandlesForUser(main, appIdentifierWithStorage, userId); + String userId, boolean revokeSessionsForLinkedAccounts) + throws StorageQueryException { + String[] sessionHandles = getAllNonExpiredSessionHandlesForUser(main, appIdentifierWithStorage, userId, + revokeSessionsForLinkedAccounts); return revokeSessionUsingSessionHandles(main, appIdentifierWithStorage, sessionHandles); } public static String[] revokeAllSessionsForUser(Main main, TenantIdentifierWithStorage tenantIdentifierWithStorage, - String userId) throws StorageQueryException { - String[] sessionHandles = getAllNonExpiredSessionHandlesForUser(tenantIdentifierWithStorage, userId); - return revokeSessionUsingSessionHandles(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(), sessionHandles); + String userId, boolean revokeSessionsForLinkedAccounts) + throws StorageQueryException { + String[] sessionHandles = getAllNonExpiredSessionHandlesForUser(tenantIdentifierWithStorage, userId, + revokeSessionsForLinkedAccounts); + return revokeSessionUsingSessionHandles(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(), + sessionHandles); } @TestOnly @@ -781,28 +833,45 @@ public static String[] getAllNonExpiredSessionHandlesForUser(Main main, String u throws StorageQueryException { Storage storage = StorageLayer.getStorage(main); return getAllNonExpiredSessionHandlesForUser(main, - new AppIdentifierWithStorage(null, null, storage), userId); + new AppIdentifierWithStorage(null, null, storage), userId, true); } public static String[] getAllNonExpiredSessionHandlesForUser( - Main main, AppIdentifierWithStorage appIdentifierWithStorage, String userId) + Main main, AppIdentifierWithStorage appIdentifierWithStorage, String userId, + boolean fetchSessionsForAllLinkedAccounts) throws StorageQueryException { TenantConfig[] tenants = Multitenancy.getAllTenantsForApp( appIdentifierWithStorage, main); List sessionHandles = new ArrayList<>(); - for (TenantConfig tenant: tenants) { - TenantIdentifierWithStorage tenantIdentifierWithStorage = null; - try { - tenantIdentifierWithStorage = tenant.tenantIdentifier.withStorage( - StorageLayer.getStorage(tenant.tenantIdentifier, main)); - sessionHandles.addAll(Arrays.asList(getAllNonExpiredSessionHandlesForUser( - tenantIdentifierWithStorage, userId))); + Set userIds = new HashSet<>(); + userIds.add(userId); + if (fetchSessionsForAllLinkedAccounts) { + if (appIdentifierWithStorage.getStorage().getType().equals(STORAGE_TYPE.SQL)) { + AuthRecipeUserInfo primaryUser = appIdentifierWithStorage.getAuthRecipeStorage() + .getPrimaryUserById(appIdentifierWithStorage, userId); + if (primaryUser != null) { + for (LoginMethod lM : primaryUser.loginMethods) { + userIds.add(lM.getSupertokensUserId()); + } + } + } + } - } catch (TenantOrAppNotFoundException e) { - // this might happen when a tenant was deleted after the tenant list was fetched - // it is okay to exclude that tenant in the results here + for (String currUserId : userIds) { + for (TenantConfig tenant : tenants) { + TenantIdentifierWithStorage tenantIdentifierWithStorage = null; + try { + tenantIdentifierWithStorage = tenant.tenantIdentifier.withStorage( + StorageLayer.getStorage(tenant.tenantIdentifier, main)); + sessionHandles.addAll(Arrays.asList(getAllNonExpiredSessionHandlesForUser( + tenantIdentifierWithStorage, currUserId, false))); + + } catch (TenantOrAppNotFoundException e) { + // this might happen when a tenant was deleted after the tenant list was fetched + // it is okay to exclude that tenant in the results here + } } } @@ -810,10 +879,26 @@ public static String[] getAllNonExpiredSessionHandlesForUser( } public static String[] getAllNonExpiredSessionHandlesForUser( - TenantIdentifierWithStorage tenantIdentifierWithStorage, String userId) + TenantIdentifierWithStorage tenantIdentifierWithStorage, String userId, + boolean fetchSessionsForAllLinkedAccounts) throws StorageQueryException { - return tenantIdentifierWithStorage.getSessionStorage() - .getAllNonExpiredSessionHandlesForUser(tenantIdentifierWithStorage, userId); + Set userIds = new HashSet<>(); + userIds.add(userId); + if (fetchSessionsForAllLinkedAccounts) { + AuthRecipeUserInfo primaryUser = tenantIdentifierWithStorage.getAuthRecipeStorage() + .getPrimaryUserById(tenantIdentifierWithStorage.toAppIdentifier(), userId); + if (primaryUser != null) { + for (LoginMethod lM : primaryUser.loginMethods) { + userIds.add(lM.getSupertokensUserId()); + } + } + } + List sessionHandles = new ArrayList<>(); + for (String currUserId : userIds) { + sessionHandles.addAll(List.of(tenantIdentifierWithStorage.getSessionStorage() + .getAllNonExpiredSessionHandlesForUser(tenantIdentifierWithStorage, currUserId))); + } + return sessionHandles.toArray(new String[0]); } @TestOnly @@ -900,7 +985,8 @@ public static void updateSession(TenantIdentifierWithStorage tenantIdentifierWit String sessionHandle, @Nullable JsonObject sessionData, @Nullable JsonObject jwtData, AccessToken.VERSION version) throws StorageQueryException, UnauthorisedException, AccessTokenPayloadError { - if (jwtData != null && Arrays.stream(AccessTokenInfo.getRequiredAndProtectedProps(version)).anyMatch(jwtData::has)) { + if (jwtData != null && + Arrays.stream(AccessTokenInfo.getRequiredAndProtectedProps(version)).anyMatch(jwtData::has)) { throw new AccessTokenPayloadError("The user payload contains protected field"); } diff --git a/src/main/java/io/supertokens/session/accessToken/AccessToken.java b/src/main/java/io/supertokens/session/accessToken/AccessToken.java index ad5fcb847..efb87b2ae 100644 --- a/src/main/java/io/supertokens/session/accessToken/AccessToken.java +++ b/src/main/java/io/supertokens/session/accessToken/AccessToken.java @@ -209,7 +209,7 @@ public static TokenInfo createNewAccessToken(@Nonnull Main main, NoSuchAlgorithmException, InvalidKeySpecException, SignatureException, UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError { try { - return createNewAccessToken(new TenantIdentifier(null, null, null), main, sessionHandle, userId, + return createNewAccessToken(new TenantIdentifier(null, null, null), main, sessionHandle, userId, userId, refreshTokenHash1, parentRefreshTokenHash1, userData, antiCsrfToken, expiryTime, version, useStaticKey); @@ -220,7 +220,8 @@ public static TokenInfo createNewAccessToken(@Nonnull Main main, public static TokenInfo createNewAccessToken(TenantIdentifier tenantIdentifier, @Nonnull Main main, @Nonnull String sessionHandle, - @Nonnull String userId, @Nonnull String refreshTokenHash1, + @Nonnull String recipeUserId, @Nonnull String primaryUserId, + @Nonnull String refreshTokenHash1, @Nullable String parentRefreshTokenHash1, @Nonnull JsonObject userData, @Nullable String antiCsrfToken, @Nullable Long expiryTime, VERSION version, boolean useStaticKey) @@ -237,7 +238,8 @@ public static TokenInfo createNewAccessToken(TenantIdentifier tenantIdentifier, } else { expires = now + Config.getConfig(tenantIdentifier, main).getAccessTokenValidity(); } - AccessTokenInfo accessToken = new AccessTokenInfo(sessionHandle, userId, refreshTokenHash1, expires, + AccessTokenInfo accessToken = new AccessTokenInfo(sessionHandle, recipeUserId, primaryUserId, refreshTokenHash1, + expires, parentRefreshTokenHash1, userData, antiCsrfToken, now, version, tenantIdentifier); JWTSigningKeyInfo keyToUse; @@ -296,7 +298,8 @@ public static TokenInfo createNewAccessTokenV1(TenantIdentifier tenantIdentifier AccessTokenInfo accessToken; long expiryTime = now + Config.getConfig(tenantIdentifier, main).getAccessTokenValidity(); - accessToken = new AccessTokenInfo(sessionHandle, userId, refreshTokenHash1, expiryTime, parentRefreshTokenHash1, + accessToken = new AccessTokenInfo(sessionHandle, userId, userId, refreshTokenHash1, expiryTime, + parentRefreshTokenHash1, userData, antiCsrfToken, now, VERSION.V1, tenantIdentifier); String token = JWT.createAndSignLegacyAccessToken(accessToken.toJSON(), signingKey.privateKey, @@ -310,6 +313,9 @@ public static VERSION getAccessTokenVersion(AccessTokenInfo accessToken) { } public static VERSION getAccessTokenVersionForCDI(SemVer version) { + if (version.greaterThanOrEqualTo(SemVer.v4_0)) { + return AccessToken.VERSION.V5; + } if (version.greaterThanOrEqualTo(SemVer.v3_0)) { return AccessToken.VERSION.V4; } @@ -354,11 +360,24 @@ public static class AccessTokenInfo { "antiCsrfToken", "tId" }; + static String[] requiredAndProtectedPropsV5 = { + "sub", + "exp", + "iat", + "sessionHandle", + "refreshTokenHash1", + "parentRefreshTokenHash1", + "antiCsrfToken", + "tId", + "rsub" + }; @Nonnull public final String sessionHandle; @Nonnull - public final String userId; + public final String recipeUserId; + @Nonnull + public final String primaryUserId; @Nonnull public final String refreshTokenHash1; @Nullable @@ -376,12 +395,14 @@ public static class AccessTokenInfo { @Nonnull public TenantIdentifier tenantIdentifier; - AccessTokenInfo(@Nonnull String sessionHandle, @Nonnull String userId, @Nonnull String refreshTokenHash1, + AccessTokenInfo(@Nonnull String sessionHandle, @Nonnull String recipeUserId, @Nonnull String primaryUserId, + @Nonnull String refreshTokenHash1, long expiryTime, @Nullable String parentRefreshTokenHash1, @Nonnull JsonObject userData, @Nullable String antiCsrfToken, long timeCreated, @Nonnull VERSION version, TenantIdentifier tenantIdentifier) { this.sessionHandle = sessionHandle; - this.userId = userId; + this.recipeUserId = recipeUserId; + this.primaryUserId = primaryUserId; this.refreshTokenHash1 = refreshTokenHash1; if (version == VERSION.V2 || version == VERSION.V1) { this.expiryTime = expiryTime; @@ -431,9 +452,17 @@ static AccessTokenInfo fromJSON(AppIdentifier appIdentifier, JsonObject payload, appIdentifier.getAppId(), payload.get("tId").getAsString()); } + String primaryUserId = payload.get("sub").getAsString(); + String recipeUserId = payload.get("sub").getAsString(); + if (version != VERSION.V3 && version != VERSION.V4) { + // this means >= v5 + recipeUserId = payload.get("rsub").getAsString(); + } + return new AccessTokenInfo( payload.get("sessionHandle").getAsString(), - payload.get("sub").getAsString(), + recipeUserId, + primaryUserId, payload.get("refreshTokenHash1").getAsString(), payload.get("exp").getAsLong() * 1000, parentRefreshTokenHash == null || parentRefreshTokenHash.isJsonNull() ? null : @@ -448,6 +477,7 @@ static AccessTokenInfo fromJSON(AppIdentifier appIdentifier, JsonObject payload, return new AccessTokenInfo( payload.get("sessionHandle").getAsString(), payload.get("userId").getAsString(), + payload.get("userId").getAsString(), payload.get("refreshTokenHash1").getAsString(), payload.get("expiryTime").getAsLong(), parentRefreshTokenHash == null || parentRefreshTokenHash.isJsonNull() ? null : @@ -465,15 +495,19 @@ static AccessTokenInfo fromJSON(AppIdentifier appIdentifier, JsonObject payload, JsonObject toJSON() throws AccessTokenPayloadError { JsonObject res = new JsonObject(); if (this.version != VERSION.V1 && this.version != VERSION.V2) { - res.addProperty("sub", this.userId); + res.addProperty("sub", this.primaryUserId); res.addProperty("exp", this.expiryTime / 1000); res.addProperty("iat", this.timeCreated / 1000); if (this.version != VERSION.V3) { res.addProperty("tId", this.tenantIdentifier.getTenantId()); } + if (this.version != VERSION.V3 && this.version != VERSION.V4) { + // this means >= v5 + res.addProperty("rsub", this.recipeUserId); + } } else { - res.addProperty("userId", this.userId); + res.addProperty("userId", this.primaryUserId); res.addProperty("expiryTime", this.expiryTime); res.addProperty("timeCreated", this.timeCreated); } @@ -513,6 +547,7 @@ public static String[] getRequiredAndProtectedProps(VERSION version) { case V1, V2 -> requiredAndProtectedPropsV2; case V3 -> requiredAndProtectedPropsV3; case V4 -> requiredAndProtectedPropsV4; + case V5 -> requiredAndProtectedPropsV5; default -> throw new IllegalArgumentException("Unknown version: " + version); }; } @@ -533,7 +568,7 @@ private static void checkRequiredPropsExist(JsonObject obj, VERSION version) } public static VERSION getLatestVersion() { - return VERSION.V4; + return VERSION.V5; } public static String getVersionStringFromAccessTokenVersion(VERSION version) { @@ -541,6 +576,6 @@ public static String getVersionStringFromAccessTokenVersion(VERSION version) { } public enum VERSION { - V1, V2, V3, V4 + V1, V2, V3, V4, V5 } } diff --git a/src/main/java/io/supertokens/session/info/SessionInfo.java b/src/main/java/io/supertokens/session/info/SessionInfo.java index cbb8e8719..82ed161d4 100644 --- a/src/main/java/io/supertokens/session/info/SessionInfo.java +++ b/src/main/java/io/supertokens/session/info/SessionInfo.java @@ -27,16 +27,22 @@ public class SessionInfo { @Nonnull public final String userId; + @Nonnull + public final String recipeUserId; + @Nonnull public final JsonObject userDataInJWT; @Nonnull public final String tenantId; - public SessionInfo(@Nonnull String handle, @Nonnull String userId, @Nonnull JsonObject userDataInJWT, @Nonnull String tenantId) { + public SessionInfo(@Nonnull String handle, @Nonnull String userId, @Nonnull String recipeUserId, + @Nonnull JsonObject userDataInJWT, + @Nonnull String tenantId) { this.handle = handle; this.userId = userId; this.userDataInJWT = userDataInJWT; this.tenantId = tenantId; + this.recipeUserId = recipeUserId; } } diff --git a/src/main/java/io/supertokens/thirdparty/ThirdParty.java b/src/main/java/io/supertokens/thirdparty/ThirdParty.java index 5300a6c2a..33a7f8717 100644 --- a/src/main/java/io/supertokens/thirdparty/ThirdParty.java +++ b/src/main/java/io/supertokens/thirdparty/ThirdParty.java @@ -17,14 +17,18 @@ package io.supertokens.thirdparty; import io.supertokens.Main; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.pluginInterface.thirdparty.UserInfo; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException; import io.supertokens.pluginInterface.thirdparty.sqlStorage.ThirdPartySQLStorage; @@ -33,16 +37,18 @@ import org.jetbrains.annotations.TestOnly; import javax.annotation.Nonnull; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; public class ThirdParty { public static class SignInUpResponse { public boolean createdNewUser; - public UserInfo user; + public AuthRecipeUserInfo user; - public SignInUpResponse(boolean createdNewUser, UserInfo user) { + public SignInUpResponse(boolean createdNewUser, AuthRecipeUserInfo user) { this.createdNewUser = createdNewUser; this.user = user; } @@ -56,16 +62,25 @@ public static SignInUpResponse signInUp2_7(TenantIdentifierWithStorage tenantIde String thirdPartyId, String thirdPartyUserId, String email, boolean isEmailVerified) throws StorageQueryException, TenantOrAppNotFoundException { - SignInUpResponse response = signInUpHelper(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, - email); + SignInUpResponse response = null; + try { + response = signInUpHelper(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, + email); + } catch (EmailChangeNotAllowedException e) { + throw new RuntimeException(e); + } if (isEmailVerified) { try { + SignInUpResponse finalResponse = response; tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { try { + // this assert is there cause this function should only be used for older CDIs in which + // account linking was not available. So loginMethod length will always be 1. + assert (finalResponse.user.loginMethods.length == 1); tenantIdentifierWithStorage.getEmailVerificationStorage() .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, - response.user.id, response.user.email, true); + finalResponse.user.getSupertokensUserId(), finalResponse.user.loginMethods[0].email, true); tenantIdentifierWithStorage.getEmailVerificationStorage() .commitTransaction(con); return null; @@ -83,7 +98,7 @@ public static SignInUpResponse signInUp2_7(TenantIdentifierWithStorage tenantIde return response; } - + @TestOnly public static SignInUpResponse signInUp2_7(Main main, String thirdPartyId, String thirdPartyUserId, String email, @@ -100,21 +115,31 @@ public static SignInUpResponse signInUp2_7(Main main, @TestOnly public static SignInUpResponse signInUp(Main main, String thirdPartyId, String thirdPartyUserId, String email) - throws StorageQueryException { + throws StorageQueryException, EmailChangeNotAllowedException { try { Storage storage = StorageLayer.getStorage(main); return signInUp( new TenantIdentifierWithStorage(null, null, null, storage), main, - thirdPartyId, thirdPartyUserId, email); + thirdPartyId, thirdPartyUserId, email, false); } catch (TenantOrAppNotFoundException | BadPermissionException e) { throw new IllegalStateException(e); } } + @TestOnly public static SignInUpResponse signInUp(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String thirdPartyId, String thirdPartyUserId, String email) - throws StorageQueryException, TenantOrAppNotFoundException, BadPermissionException { + throws StorageQueryException, TenantOrAppNotFoundException, BadPermissionException, + EmailChangeNotAllowedException { + return signInUp(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, email, false); + } + + public static SignInUpResponse signInUp(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, + String thirdPartyId, + String thirdPartyUserId, String email, boolean isEmailVerified) + throws StorageQueryException, TenantOrAppNotFoundException, BadPermissionException, + EmailChangeNotAllowedException { TenantConfig config = Multitenancy.getTenantInfo(main, tenantIdentifierWithStorage); if (config == null) { @@ -124,13 +149,45 @@ public static SignInUpResponse signInUp(TenantIdentifierWithStorage tenantIdenti throw new BadPermissionException("Third Party login not enabled for tenant"); } - return signInUpHelper(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, email); + SignInUpResponse response = signInUpHelper(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, + email); + + if (isEmailVerified) { + for (LoginMethod lM : response.user.loginMethods) { + if (lM.thirdParty != null && lM.thirdParty.id.equals(thirdPartyId) && lM.thirdParty.userId.equals(thirdPartyUserId)) { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage() + .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + lM.getSupertokensUserId(), lM.email, true); + tenantIdentifierWithStorage.getEmailVerificationStorage() + .commitTransaction(con); + + return null; + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } + }); + lM.setVerified(); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } + throw new StorageQueryException(e); + } + break; + } + } + } + + return response; } private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String thirdPartyId, String thirdPartyUserId, String email) throws StorageQueryException, - TenantOrAppNotFoundException { + TenantOrAppNotFoundException, EmailChangeNotAllowedException { ThirdPartySQLStorage storage = tenantIdentifierWithStorage.getThirdPartyStorage(); while (true) { // loop for sign in + sign up @@ -141,7 +198,8 @@ private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenan long timeJoined = System.currentTimeMillis(); try { - UserInfo createdUser = storage.signUp(tenantIdentifierWithStorage, userId, email, new UserInfo.ThirdParty(thirdPartyId, thirdPartyUserId), timeJoined); + AuthRecipeUserInfo createdUser = storage.signUp(tenantIdentifierWithStorage, userId, email, + new LoginMethod.ThirdParty(thirdPartyId, thirdPartyUserId), timeJoined); return new SignInUpResponse(true, createdUser); } catch (DuplicateUserIdException e) { @@ -153,69 +211,157 @@ private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenan } // we try to get user and update their email - SignInUpResponse response = null; - try { - // We should update the user email based on thirdPartyId and thirdPartyUserId across all apps, - // so we iterate through all the app storages and do the update. - // Note that we are only locking for each storage, and no global app wide lock, but should be okay - // because same user parallelly logging into different tenants at the same time with different email - // is a rare situation - AppIdentifier appIdentifier = tenantIdentifierWithStorage.toAppIdentifier(); - Storage[] storages = StorageLayer.getStoragesForApp(main, appIdentifier); - for (Storage st : storages) { - storage.startTransaction(con -> { - UserInfo user = storage.getUserInfoUsingId_Transaction(appIdentifier.withStorage(st), con, - thirdPartyId, thirdPartyUserId); - - if (user == null) { - storage.commitTransaction(con); - return null; + AppIdentifier appIdentifier = tenantIdentifierWithStorage.toAppIdentifier(); + AuthRecipeSQLStorage authRecipeStorage = + (AuthRecipeSQLStorage) tenantIdentifierWithStorage.getAuthRecipeStorage(); + + { // Try without transaction, because in most cases we might not need to update the email + AuthRecipeUserInfo userFromDb = null; + + AuthRecipeUserInfo[] usersFromDb = authRecipeStorage.listPrimaryUsersByThirdPartyInfo( + appIdentifier, + thirdPartyId, thirdPartyUserId); + for (AuthRecipeUserInfo user : usersFromDb) { + if (user.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { + if (userFromDb != null) { + throw new IllegalStateException("Should never happen"); } + userFromDb = user; + } + } + if (userFromDb == null) { + continue; // try to create the user again + } - if (!email.equals(user.email)) { - storage.updateUserEmail_Transaction(appIdentifier.withStorage(st), con, - thirdPartyId, thirdPartyUserId, email); - } + LoginMethod lM = null; + for (LoginMethod loginMethod : userFromDb.loginMethods) { + if (loginMethod.thirdParty != null && loginMethod.thirdParty.id.equals(thirdPartyId) && + loginMethod.thirdParty.userId.equals(thirdPartyUserId)) { + lM = loginMethod; + break; + } + } - storage.commitTransaction(con); - return null; - }); + if (lM == null) { + throw new IllegalStateException("Should never come here"); } - UserInfo user = getUser(tenantIdentifierWithStorage, thirdPartyId, thirdPartyUserId); - return new SignInUpResponse(false, user); - } catch (StorageTransactionLogicException ignored) { - } + if (email.equals(lM.email)) { + return new SignInUpResponse(false, userFromDb); + } else { + // Email needs updating, so repeat everything in a transaction + try { - if (response != null) { - return response; + storage.startTransaction(con -> { + AuthRecipeUserInfo userFromDb1 = null; + + AuthRecipeUserInfo[] usersFromDb1 = authRecipeStorage.listPrimaryUsersByThirdPartyInfo_Transaction( + appIdentifier, + con, + thirdPartyId, thirdPartyUserId); + for (AuthRecipeUserInfo user : usersFromDb1) { + if (user.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { + if (userFromDb1 != null) { + throw new IllegalStateException("Should never happen"); + } + userFromDb1 = user; + } + } + + if (userFromDb1 == null) { + storage.commitTransaction(con); + return null; + } + + LoginMethod lM1 = null; + for (LoginMethod loginMethod : userFromDb1.loginMethods) { + if (loginMethod.thirdParty != null && loginMethod.thirdParty.id.equals(thirdPartyId) && + loginMethod.thirdParty.userId.equals(thirdPartyUserId)) { + lM1 = loginMethod; + break; + } + } + + if (lM1 == null) { + throw new IllegalStateException("Should never come here"); + } + + if (!email.equals(lM1.email)) { + // before updating the email, we must check for if another primary user has the same + // email, and if they do, then we do not allow the update. + if (userFromDb1.isPrimaryUser) { + for (String tenantId : userFromDb1.tenantIds) { + AuthRecipeUserInfo[] userBasedOnEmail = + authRecipeStorage.listPrimaryUsersByEmail_Transaction( + appIdentifier, con, email + ); + for (AuthRecipeUserInfo userWithSameEmail : userBasedOnEmail) { + if (!userWithSameEmail.tenantIds.contains(tenantId)) { + continue; + } + if (userWithSameEmail.isPrimaryUser && + !userWithSameEmail.getSupertokensUserId().equals(userFromDb1.getSupertokensUserId())) { + throw new StorageTransactionLogicException( + new EmailChangeNotAllowedException()); + } + } + } + } + storage.updateUserEmail_Transaction(appIdentifier, con, + thirdPartyId, thirdPartyUserId, email); + } + + storage.commitTransaction(con); + return null; + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof EmailChangeNotAllowedException) { + throw (EmailChangeNotAllowedException) e.actualException; + } + throw new StorageQueryException(e); + } + } } - // retry.. + AuthRecipeUserInfo user = getUser(tenantIdentifierWithStorage, thirdPartyId, thirdPartyUserId); + return new SignInUpResponse(false, user); } } - public static UserInfo getUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId) + @Deprecated + public static AuthRecipeUserInfo getUser(AppIdentifierWithStorage appIdentifierWithStorage, String userId) throws StorageQueryException { - return appIdentifierWithStorage.getThirdPartyStorage() - .getThirdPartyUserInfoUsingId(appIdentifierWithStorage, userId); + AuthRecipeUserInfo result = appIdentifierWithStorage.getAuthRecipeStorage() + .getPrimaryUserById(appIdentifierWithStorage, userId); + if (result == null) { + return null; + } + for (LoginMethod lM : result.loginMethods) { + if (lM.getSupertokensUserId().equals(userId) && lM.recipeId == RECIPE_ID.THIRD_PARTY) { + return AuthRecipeUserInfo.create(lM.getSupertokensUserId(), result.isPrimaryUser, + lM); + } + } + return null; } + @Deprecated @TestOnly - public static UserInfo getUser(Main main, String userId) throws StorageQueryException { + public static AuthRecipeUserInfo getUser(Main main, String userId) throws StorageQueryException { Storage storage = StorageLayer.getStorage(main); return getUser(new AppIdentifierWithStorage(null, null, storage), userId); } - public static UserInfo getUser(TenantIdentifierWithStorage tenantIdentifierWithStorage, String thirdPartyId, - String thirdPartyUserId) + public static AuthRecipeUserInfo getUser(TenantIdentifierWithStorage tenantIdentifierWithStorage, + String thirdPartyId, + String thirdPartyUserId) throws StorageQueryException { return tenantIdentifierWithStorage.getThirdPartyStorage() - .getThirdPartyUserInfoUsingId(tenantIdentifierWithStorage, thirdPartyId, thirdPartyUserId); + .getPrimaryUserByThirdPartyInfo(tenantIdentifierWithStorage, thirdPartyId, thirdPartyUserId); } @TestOnly - public static UserInfo getUser(Main main, String thirdPartyId, String thirdPartyUserId) + public static AuthRecipeUserInfo getUser(Main main, String thirdPartyId, String thirdPartyUserId) throws StorageQueryException { Storage storage = StorageLayer.getStorage(main); return getUser( @@ -223,11 +369,21 @@ public static UserInfo getUser(Main main, String thirdPartyId, String thirdParty thirdPartyId, thirdPartyUserId); } - public static UserInfo[] getUsersByEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage, - @Nonnull String email) + @Deprecated + public static AuthRecipeUserInfo[] getUsersByEmail(TenantIdentifierWithStorage tenantIdentifierWithStorage, + @Nonnull String email) throws StorageQueryException { - return tenantIdentifierWithStorage.getThirdPartyStorage() - .getThirdPartyUsersByEmail(tenantIdentifierWithStorage, email); + AuthRecipeUserInfo[] users = tenantIdentifierWithStorage.getThirdPartyStorage() + .listPrimaryUsersByEmail(tenantIdentifierWithStorage, email); + List result = new ArrayList<>(); + for (AuthRecipeUserInfo user : users) { + for (LoginMethod lM : user.loginMethods) { + if (lM.recipeId == RECIPE_ID.THIRD_PARTY && lM.email.equals(email)) { + result.add(user); + } + } + } + return result.toArray(new AuthRecipeUserInfo[0]); } public static void verifyThirdPartyProvidersArray(ThirdPartyConfig.Provider[] providers) diff --git a/src/main/java/io/supertokens/useridmapping/UserIdMapping.java b/src/main/java/io/supertokens/useridmapping/UserIdMapping.java index f613f73fb..687e8fea2 100644 --- a/src/main/java/io/supertokens/useridmapping/UserIdMapping.java +++ b/src/main/java/io/supertokens/useridmapping/UserIdMapping.java @@ -20,18 +20,23 @@ import io.supertokens.Main; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.emailverification.EmailVerificationStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.jwt.JWTRecipeStorage; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.session.SessionStorage; +import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; import io.supertokens.pluginInterface.totp.TOTPStorage; import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage; import io.supertokens.pluginInterface.useridmapping.exception.UnknownSuperTokensUserIdException; import io.supertokens.pluginInterface.useridmapping.exception.UserIdMappingAlreadyExistsException; +import io.supertokens.pluginInterface.useridmapping.sqlStorage.UserIdMappingSQLStorage; import io.supertokens.pluginInterface.usermetadata.UserMetadataStorage; import io.supertokens.pluginInterface.userroles.UserRolesStorage; import io.supertokens.storageLayer.StorageLayer; @@ -42,6 +47,8 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; public class UserIdMapping { @@ -120,17 +127,38 @@ public static io.supertokens.pluginInterface.useridmapping.UserIdMapping getUser AppIdentifierWithStorage appIdentifierWithStorage, String userId, UserIdType userIdType) throws StorageQueryException { - UserIdMappingStorage storage = appIdentifierWithStorage.getUserIdMappingStorage(); + UserIdMappingSQLStorage storage = (UserIdMappingSQLStorage) appIdentifierWithStorage.getUserIdMappingStorage(); + + try { + return storage.startTransaction(con -> { + return getUserIdMapping(con, appIdentifierWithStorage, userId, userIdType); + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof StorageQueryException) { + throw (StorageQueryException) e.actualException; + } else { + throw new IllegalStateException(e.actualException); + } + } + } + + public static io.supertokens.pluginInterface.useridmapping.UserIdMapping getUserIdMapping( + TransactionConnection con, + AppIdentifierWithStorage appIdentifierWithStorage, String userId, + UserIdType userIdType) + throws StorageQueryException { + UserIdMappingSQLStorage storage = (UserIdMappingSQLStorage) appIdentifierWithStorage.getUserIdMappingStorage(); if (userIdType == UserIdType.SUPERTOKENS) { - return storage.getUserIdMapping(appIdentifierWithStorage, userId, true); + return storage.getUserIdMapping_Transaction(con, appIdentifierWithStorage, userId, true); } + if (userIdType == UserIdType.EXTERNAL) { - return storage.getUserIdMapping(appIdentifierWithStorage, userId, false); + return storage.getUserIdMapping_Transaction(con, appIdentifierWithStorage, userId, false); } - io.supertokens.pluginInterface.useridmapping.UserIdMapping[] userIdMappings = storage.getUserIdMapping( - appIdentifierWithStorage, userId); + io.supertokens.pluginInterface.useridmapping.UserIdMapping[] userIdMappings = storage.getUserIdMapping_Transaction( + con, appIdentifierWithStorage, userId); if (userIdMappings.length == 0) { return null; @@ -261,6 +289,14 @@ public static HashMap getUserIdMappingForSuperTokensUserIds( return tenantIdentifierWithStorage.getUserIdMappingStorage().getUserIdMappingForSuperTokensIds(userIds); } + public static HashMap getUserIdMappingForSuperTokensUserIds( + AppIdentifierWithStorage appIdentifierWithStorage, + ArrayList userIds) + throws StorageQueryException { + // userIds are already filtered for a tenant, so this becomes a tenant specific operation. + return appIdentifierWithStorage.getUserIdMappingStorage().getUserIdMappingForSuperTokensIds(userIds); + } + @TestOnly public static HashMap getUserIdMappingForSuperTokensUserIds(Main main, ArrayList userIds) @@ -321,4 +357,54 @@ public static void assertThatUserIdIsNotBeingUsedInNonAuthRecipes( } } } + + public static void populateExternalUserIdForUsers(AppIdentifierWithStorage appIdentifierWithStorage, AuthRecipeUserInfo[] users) + throws StorageQueryException { + Set userIds = new HashSet<>(); + + for (AuthRecipeUserInfo user : users) { + userIds.add(user.getSupertokensUserId()); + + for (LoginMethod lm : user.loginMethods) { + userIds.add(lm.getSupertokensUserId()); + } + } + ArrayList userIdsList = new ArrayList<>(userIds); + userIdsList.addAll(userIds); + HashMap userIdMappings = getUserIdMappingForSuperTokensUserIds(appIdentifierWithStorage, + userIdsList); + + for (AuthRecipeUserInfo user : users) { + user.setExternalUserId(userIdMappings.get(user.getSupertokensUserId())); + + for (LoginMethod lm : user.loginMethods) { + lm.setExternalUserId(userIdMappings.get(lm.getSupertokensUserId())); + } + } + } + + public static void populateExternalUserIdForUsers(TenantIdentifierWithStorage tenantIdentifierWithStorage, AuthRecipeUserInfo[] users) + throws StorageQueryException { + Set userIds = new HashSet<>(); + + for (AuthRecipeUserInfo user : users) { + userIds.add(user.getSupertokensUserId()); + + for (LoginMethod lm : user.loginMethods) { + userIds.add(lm.getSupertokensUserId()); + } + } + ArrayList userIdsList = new ArrayList<>(userIds); + userIdsList.addAll(userIds); + HashMap userIdMappings = getUserIdMappingForSuperTokensUserIds(tenantIdentifierWithStorage, + userIdsList); + + for (AuthRecipeUserInfo user : users) { + user.setExternalUserId(userIdMappings.get(user.getSupertokensUserId())); + + for (LoginMethod lm : user.loginMethods) { + lm.setExternalUserId(userIdMappings.get(lm.getSupertokensUserId())); + } + } + } } diff --git a/src/main/java/io/supertokens/utils/SemVer.java b/src/main/java/io/supertokens/utils/SemVer.java index e5ab842d3..64b63ace6 100644 --- a/src/main/java/io/supertokens/utils/SemVer.java +++ b/src/main/java/io/supertokens/utils/SemVer.java @@ -33,6 +33,7 @@ public class SemVer implements Comparable { public static final SemVer v2_20 = new SemVer("2.20"); public static final SemVer v2_21 = new SemVer("2.21"); public static final SemVer v3_0 = new SemVer("3.0"); + public static final SemVer v4_0 = new SemVer("4.0"); final private String version; @@ -41,11 +42,11 @@ public final String get() { } public SemVer(String version) { - if(version == null) { + if (version == null) { throw new IllegalArgumentException("Version can not be null"); } - if(!version.matches("[0-9]+(\\.[0-9]+)*")) { + if (!version.matches("[0-9]+(\\.[0-9]+)*")) { throw new IllegalArgumentException("Invalid version format"); } @@ -68,7 +69,8 @@ public boolean lesserThan(SemVer max) { return this.compareTo(max) < 0; } - @Override public int compareTo(SemVer that) { + @Override + public int compareTo(SemVer that) { if (that == null) { return 1; } @@ -82,11 +84,11 @@ public boolean lesserThan(SemVer max) { int thisPart = i < thisParts.length ? Integer.parseInt(thisParts[i]) : 0; int thatPart = i < thatParts.length ? Integer.parseInt(thatParts[i]) : 0; - if(thisPart < thatPart) { + if (thisPart < thatPart) { return -1; } - if(thisPart > thatPart) { + if (thisPart > thatPart) { return 1; } } @@ -94,16 +96,17 @@ public boolean lesserThan(SemVer max) { return 0; } - @Override public boolean equals(Object that) { - if(this == that) { + @Override + public boolean equals(Object that) { + if (this == that) { return true; } - if(that == null) { + if (that == null) { return false; } - if(!(that instanceof SemVer)) { + if (!(that instanceof SemVer)) { return false; } diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index 8184bde18..5e4a4ff8c 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -26,6 +26,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.api.accountlinking.*; import io.supertokens.webserver.api.core.*; import io.supertokens.webserver.api.dashboard.*; import io.supertokens.webserver.api.emailpassword.UserAPI; @@ -37,10 +38,6 @@ import io.supertokens.webserver.api.jwt.JWKSAPI; import io.supertokens.webserver.api.jwt.JWTSigningAPI; import io.supertokens.webserver.api.multitenancy.*; -import io.supertokens.webserver.api.multitenancy.CreateOrUpdateAppAPI; -import io.supertokens.webserver.api.multitenancy.CreateOrUpdateConnectionUriDomainAPI; -import io.supertokens.webserver.api.multitenancy.CreateOrUpdateTenantOrGetTenantAPI; -import io.supertokens.webserver.api.multitenancy.RemoveTenantAPI; import io.supertokens.webserver.api.multitenancy.thirdparty.CreateOrUpdateThirdPartyConfigAPI; import io.supertokens.webserver.api.multitenancy.thirdparty.RemoveThirdPartyConfigAPI; import io.supertokens.webserver.api.passwordless.*; @@ -250,6 +247,16 @@ private void setupRoutes() { addAPI(new AssociateUserToTenantAPI(main)); addAPI(new DisassociateUserFromTenant(main)); + addAPI(new GetUserByIdAPI(main)); + addAPI(new ListUsersByAccountInfoAPI(main)); + + addAPI(new CanCreatePrimaryUserAPI(main)); + addAPI(new CreatePrimaryUserAPI(main)); + addAPI(new CanLinkAccountsAPI(main)); + addAPI(new LinkAccountsAPI(main)); + addAPI(new UnlinkAccountAPI(main)); + addAPI(new ConsumeResetPasswordAPI(main)); + StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); diff --git a/src/main/java/io/supertokens/webserver/WebserverAPI.java b/src/main/java/io/supertokens/webserver/WebserverAPI.java index ec9c876ce..fb20f98fe 100644 --- a/src/main/java/io/supertokens/webserver/WebserverAPI.java +++ b/src/main/java/io/supertokens/webserver/WebserverAPI.java @@ -75,10 +75,11 @@ public abstract class WebserverAPI extends HttpServlet { supportedVersions.add(SemVer.v2_20); supportedVersions.add(SemVer.v2_21); supportedVersions.add(SemVer.v3_0); + supportedVersions.add(SemVer.v4_0); } public static SemVer getLatestCDIVersion() { - return SemVer.v3_0; + return SemVer.v4_0; } public SemVer getLatestCDIVersionForRequest(HttpServletRequest req) diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/CanCreatePrimaryUserAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/CanCreatePrimaryUserAPI.java new file mode 100644 index 000000000..9457ddd0d --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/accountlinking/CanCreatePrimaryUserAPI.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.webserver.api.accountlinking; + +import com.google.gson.JsonObject; +import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; +import io.supertokens.Main; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; +import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithPrimaryUserIdException; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.useridmapping.UserIdType; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class CanCreatePrimaryUserAPI extends WebserverAPI { + + public CanCreatePrimaryUserAPI(Main main) { + super(main, RECIPE_ID.ACCOUNT_LINKING.toString()); + } + + @Override + public String getPath() { + return "/recipe/accountlinking/user/primary/check"; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // API is app specific + String inputRecipeUserId = InputParser.getQueryParamOrThrowError(req, "recipeUserId", false); + + AppIdentifierWithStorage appIdentifierWithStorage = null; + try { + String userId = inputRecipeUserId; + AppIdentifierWithStorageAndUserIdMapping mappingAndStorage = + getAppIdentifierWithStorageAndUserIdMappingFromRequest( + req, inputRecipeUserId, UserIdType.ANY); + if (mappingAndStorage.userIdMapping != null) { + userId = mappingAndStorage.userIdMapping.superTokensUserId; + } + appIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage; + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.canCreatePrimaryUser(appIdentifierWithStorage, + userId); + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + response.addProperty("wasAlreadyAPrimaryUser", result.wasAlreadyAPrimaryUser); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException | TenantOrAppNotFoundException e) { + throw new ServletException(e); + } catch (UnknownUserIdException e) { + throw new ServletException(new BadRequestException("Unknown user ID provided")); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + try { + JsonObject response = new JsonObject(); + response.addProperty("status", "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); + io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping( + appIdentifierWithStorage, e.primaryUserId, + UserIdType.SUPERTOKENS); + if (result != null) { + response.addProperty("primaryUserId", result.externalUserId); + } else { + response.addProperty("primaryUserId", e.primaryUserId); + } + response.addProperty("description", e.getMessage()); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException ex) { + throw new ServletException(ex); + } + } catch (RecipeUserIdAlreadyLinkedWithPrimaryUserIdException e) { + try { + JsonObject response = new JsonObject(); + response.addProperty("status", "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"); + io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping( + appIdentifierWithStorage, e.primaryUserId, + UserIdType.SUPERTOKENS); + if (result != null) { + response.addProperty("primaryUserId", result.externalUserId); + } else { + response.addProperty("primaryUserId", e.primaryUserId); + } + response.addProperty("description", e.getMessage()); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException ex) { + throw new ServletException(ex); + } + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/CanLinkAccountsAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/CanLinkAccountsAPI.java new file mode 100644 index 000000000..97c10f2c6 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/accountlinking/CanLinkAccountsAPI.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.webserver.api.accountlinking; + +import com.google.gson.JsonObject; +import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; +import io.supertokens.Main; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; +import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException; +import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.useridmapping.UserIdType; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class CanLinkAccountsAPI extends WebserverAPI { + + public CanLinkAccountsAPI(Main main) { + super(main, RECIPE_ID.ACCOUNT_LINKING.toString()); + } + + @Override + public String getPath() { + return "/recipe/accountlinking/user/link/check"; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // API is app specific + String inputRecipeUserId = InputParser.getQueryParamOrThrowError(req, "recipeUserId", false); + String inputPrimaryUserId = InputParser.getQueryParamOrThrowError(req, "primaryUserId", false); + + AppIdentifierWithStorage primaryUserIdAppIdentifierWithStorage = null; + AppIdentifierWithStorage recipeUserIdAppIdentifierWithStorage = null; + try { + String recipeUserId = inputRecipeUserId; + { + AppIdentifierWithStorageAndUserIdMapping mappingAndStorage = + getAppIdentifierWithStorageAndUserIdMappingFromRequest( + req, inputRecipeUserId, UserIdType.ANY); + if (mappingAndStorage.userIdMapping != null) { + recipeUserId = mappingAndStorage.userIdMapping.superTokensUserId; + } + recipeUserIdAppIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage; + } + String primaryUserId = inputPrimaryUserId; + { + AppIdentifierWithStorageAndUserIdMapping mappingAndStorage = + getAppIdentifierWithStorageAndUserIdMappingFromRequest( + req, inputPrimaryUserId, UserIdType.ANY); + if (mappingAndStorage.userIdMapping != null) { + primaryUserId = mappingAndStorage.userIdMapping.superTokensUserId; + } + primaryUserIdAppIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage; + } + + // we do a check based on user pool ID and not instance reference checks cause the user + // could be in the same db, but their storage layers may just have different + if (!primaryUserIdAppIdentifierWithStorage.getStorage().getUserPoolId().equals( + recipeUserIdAppIdentifierWithStorage.getStorage().getUserPoolId())) { + throw new ServletException( + new BadRequestException( + "Cannot link users that are parts of different databases. Different pool IDs: " + + primaryUserIdAppIdentifierWithStorage.getStorage().getUserPoolId() + " AND " + + recipeUserIdAppIdentifierWithStorage.getStorage().getUserPoolId())); + } + + AuthRecipe.CanLinkAccountsResult result = AuthRecipe.canLinkAccounts(primaryUserIdAppIdentifierWithStorage, + recipeUserId, primaryUserId); + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + response.addProperty("accountsAlreadyLinked", result.alreadyLinked); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException | TenantOrAppNotFoundException e) { + throw new ServletException(e); + } catch (UnknownUserIdException e) { + throw new ServletException(new BadRequestException("Unknown user ID provided")); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + try { + JsonObject response = new JsonObject(); + response.addProperty("status", "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); + io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping( + primaryUserIdAppIdentifierWithStorage, e.primaryUserId, + UserIdType.SUPERTOKENS); + if (result != null) { + response.addProperty("primaryUserId", result.externalUserId); + } else { + response.addProperty("primaryUserId", e.primaryUserId); + } + response.addProperty("description", e.getMessage()); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException ex) { + throw new ServletException(ex); + } + } catch (RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException e) { + try { + JsonObject response = new JsonObject(); + response.addProperty("status", "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); + UserIdMapping.populateExternalUserIdForUsers(recipeUserIdAppIdentifierWithStorage, new AuthRecipeUserInfo[]{e.recipeUser}); + response.addProperty("primaryUserId", e.recipeUser.getSupertokensOrExternalUserId()); + response.addProperty("description", e.getMessage()); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException ex) { + throw new ServletException(ex); + } + } catch (InputUserIdIsNotAPrimaryUserException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "INPUT_USER_IS_NOT_A_PRIMARY_USER"); + super.sendJsonResponse(200, response, resp); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/CreatePrimaryUserAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/CreatePrimaryUserAPI.java new file mode 100644 index 000000000..8573650b7 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/accountlinking/CreatePrimaryUserAPI.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.webserver.api.accountlinking; + +import com.google.gson.JsonObject; +import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; +import io.supertokens.Main; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; +import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithPrimaryUserIdException; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.useridmapping.UserIdType; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class CreatePrimaryUserAPI extends WebserverAPI { + + public CreatePrimaryUserAPI(Main main) { + super(main, RECIPE_ID.ACCOUNT_LINKING.toString()); + } + + @Override + public String getPath() { + return "/recipe/accountlinking/user/primary"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // API is app specific + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String inputRecipeUserId = InputParser.parseStringOrThrowError(input, "recipeUserId", false); + + AppIdentifierWithStorage appIdentifierWithStorage = null; + try { + String userId = inputRecipeUserId; + AppIdentifierWithStorageAndUserIdMapping mappingAndStorage = + getAppIdentifierWithStorageAndUserIdMappingFromRequest( + req, inputRecipeUserId, UserIdType.ANY); + if (mappingAndStorage.userIdMapping != null) { + userId = mappingAndStorage.userIdMapping.superTokensUserId; + } + appIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage; + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(main, appIdentifierWithStorage, + userId); + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + response.addProperty("wasAlreadyAPrimaryUser", result.wasAlreadyAPrimaryUser); + if (mappingAndStorage.userIdMapping != null) { + result.user.setExternalUserId(mappingAndStorage.userIdMapping.externalUserId); + } else { + result.user.setExternalUserId(null); + } + response.add("user", result.user.toJson()); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException e) { + throw new ServletException(e); + } catch (UnknownUserIdException e) { + throw new ServletException(new BadRequestException("Unknown user ID provided")); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + try { + JsonObject response = new JsonObject(); + response.addProperty("status", "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); + io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping( + appIdentifierWithStorage, e.primaryUserId, + UserIdType.SUPERTOKENS); + if (result != null) { + response.addProperty("primaryUserId", result.externalUserId); + } else { + response.addProperty("primaryUserId", e.primaryUserId); + } + response.addProperty("description", e.getMessage()); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException ex) { + throw new ServletException(ex); + } + } catch (RecipeUserIdAlreadyLinkedWithPrimaryUserIdException e) { + try { + JsonObject response = new JsonObject(); + response.addProperty("status", "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"); + io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping( + appIdentifierWithStorage, e.primaryUserId, + UserIdType.SUPERTOKENS); + if (result != null) { + response.addProperty("primaryUserId", result.externalUserId); + } else { + response.addProperty("primaryUserId", e.primaryUserId); + } + response.addProperty("description", e.getMessage()); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException ex) { + throw new ServletException(ex); + } + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java new file mode 100644 index 000000000..76a9d4dfe --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.webserver.api.accountlinking; + +import com.google.gson.JsonObject; +import io.supertokens.ActiveUsers; +import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; +import io.supertokens.Main; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; +import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException; +import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.useridmapping.UserIdType; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class LinkAccountsAPI extends WebserverAPI { + + public LinkAccountsAPI(Main main) { + super(main, RECIPE_ID.ACCOUNT_LINKING.toString()); + } + + @Override + public String getPath() { + return "/recipe/accountlinking/user/link"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // API is app specific + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String inputRecipeUserId = InputParser.parseStringOrThrowError(input, "recipeUserId", false); + String inputPrimaryUserId = InputParser.parseStringOrThrowError(input, "primaryUserId", false); + + AppIdentifierWithStorage primaryUserIdAppIdentifierWithStorage = null; + AppIdentifierWithStorage recipeUserIdAppIdentifierWithStorage = null; + try { + String recipeUserId = inputRecipeUserId; + { + AppIdentifierWithStorageAndUserIdMapping mappingAndStorage = + getAppIdentifierWithStorageAndUserIdMappingFromRequest( + req, inputRecipeUserId, UserIdType.ANY); + if (mappingAndStorage.userIdMapping != null) { + recipeUserId = mappingAndStorage.userIdMapping.superTokensUserId; + } + recipeUserIdAppIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage; + } + String primaryUserId = inputPrimaryUserId; + { + AppIdentifierWithStorageAndUserIdMapping mappingAndStorage = + getAppIdentifierWithStorageAndUserIdMappingFromRequest( + req, inputPrimaryUserId, UserIdType.ANY); + if (mappingAndStorage.userIdMapping != null) { + primaryUserId = mappingAndStorage.userIdMapping.superTokensUserId; + } + primaryUserIdAppIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage; + } + + // we do a check based on user pool ID and not instance reference checks cause the user + // could be in the same db, but their storage layers may just have different + if (!primaryUserIdAppIdentifierWithStorage.getStorage().getUserPoolId().equals( + recipeUserIdAppIdentifierWithStorage.getStorage().getUserPoolId())) { + throw new ServletException( + new BadRequestException( + "Cannot link users that are parts of different databases. Different pool IDs: " + + primaryUserIdAppIdentifierWithStorage.getStorage().getUserPoolId() + " AND " + + recipeUserIdAppIdentifierWithStorage.getStorage().getUserPoolId())); + } + + AuthRecipe.LinkAccountsResult linkAccountsResult = AuthRecipe.linkAccounts(main, + primaryUserIdAppIdentifierWithStorage, + recipeUserId, primaryUserId); + + UserIdMapping.populateExternalUserIdForUsers(primaryUserIdAppIdentifierWithStorage, new AuthRecipeUserInfo[]{linkAccountsResult.user}); + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + response.addProperty("accountsAlreadyLinked", linkAccountsResult.wasAlreadyLinked); + response.add("user", linkAccountsResult.user.toJson()); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException e) { + throw new ServletException(e); + } catch (UnknownUserIdException e) { + throw new ServletException(new BadRequestException("Unknown user ID provided")); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + try { + JsonObject response = new JsonObject(); + response.addProperty("status", "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); + io.supertokens.pluginInterface.useridmapping.UserIdMapping result = UserIdMapping.getUserIdMapping( + primaryUserIdAppIdentifierWithStorage, e.primaryUserId, + UserIdType.SUPERTOKENS); + if (result != null) { + response.addProperty("primaryUserId", result.externalUserId); + } else { + response.addProperty("primaryUserId", e.primaryUserId); + } + response.addProperty("description", e.getMessage()); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException ex) { + throw new ServletException(ex); + } + } catch (RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException e) { + try { + JsonObject response = new JsonObject(); + response.addProperty("status", "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); + UserIdMapping.populateExternalUserIdForUsers(recipeUserIdAppIdentifierWithStorage, new AuthRecipeUserInfo[]{e.recipeUser}); + response.addProperty("primaryUserId", e.recipeUser.getSupertokensOrExternalUserId()); + response.addProperty("description", e.getMessage()); + response.add("user", e.recipeUser.toJson()); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException ex) { + throw new ServletException(ex); + } + } catch (InputUserIdIsNotAPrimaryUserException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "INPUT_USER_IS_NOT_A_PRIMARY_USER"); + super.sendJsonResponse(200, response, resp); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/UnlinkAccountAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/UnlinkAccountAPI.java new file mode 100644 index 000000000..3abed0328 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/accountlinking/UnlinkAccountAPI.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.webserver.api.accountlinking; + +import com.google.gson.JsonObject; +import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; +import io.supertokens.Main; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.useridmapping.UserIdType; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class UnlinkAccountAPI extends WebserverAPI { + + public UnlinkAccountAPI(Main main) { + super(main, RECIPE_ID.ACCOUNT_LINKING.toString()); + } + + @Override + public String getPath() { + return "/recipe/accountlinking/user/unlink"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // API is app specific + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String inputRecipeUserId = InputParser.parseStringOrThrowError(input, "recipeUserId", false); + + AppIdentifierWithStorage appIdentifierWithStorage = null; + try { + String userId = inputRecipeUserId; + AppIdentifierWithStorageAndUserIdMapping mappingAndStorage = + getAppIdentifierWithStorageAndUserIdMappingFromRequest( + req, inputRecipeUserId, UserIdType.ANY); + if (mappingAndStorage.userIdMapping != null) { + userId = mappingAndStorage.userIdMapping.superTokensUserId; + } + appIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage; + + boolean wasDeleted = AuthRecipe.unlinkAccounts(main, appIdentifierWithStorage, + userId); + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + response.addProperty("wasRecipeUserDeleted", wasDeleted); + response.addProperty("wasLinked", true); + super.sendJsonResponse(200, response, resp); + } catch (StorageQueryException | TenantOrAppNotFoundException e) { + throw new ServletException(e); + } catch (UnknownUserIdException e) { + throw new ServletException(new BadRequestException("Unknown user ID provided")); + } catch (InputUserIdIsNotAPrimaryUserException e) { + JsonObject response = new JsonObject(); + response.addProperty("status", "OK"); + response.addProperty("wasRecipeUserDeleted", false); + response.addProperty("wasLinked", false); + super.sendJsonResponse(200, response, resp); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/core/DeleteUserAPI.java b/src/main/java/io/supertokens/webserver/api/core/DeleteUserAPI.java index 166b361da..ce51410f3 100644 --- a/src/main/java/io/supertokens/webserver/api/core/DeleteUserAPI.java +++ b/src/main/java/io/supertokens/webserver/api/core/DeleteUserAPI.java @@ -51,11 +51,18 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I // this API is app specific JsonObject input = InputParser.parseJsonObjectOrThrowError(req); String userId = InputParser.parseStringOrThrowError(input, "userId", false); + Boolean removeAllLinkedAccounts = InputParser.parseBooleanOrThrowError(input, "removeAllLinkedAccounts", true); + + if (removeAllLinkedAccounts == null) { + removeAllLinkedAccounts = true; + } + try { AppIdentifierWithStorageAndUserIdMapping appIdentifierWithStorageAndUserIdMapping = this.getAppIdentifierWithStorageAndUserIdMappingFromRequest(req, userId, UserIdType.ANY); AuthRecipe.deleteUser(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, userId, + removeAllLinkedAccounts, appIdentifierWithStorageAndUserIdMapping.userIdMapping); } catch (StorageQueryException | TenantOrAppNotFoundException | StorageTransactionLogicException e) { throw new ServletException(e); diff --git a/src/main/java/io/supertokens/webserver/api/core/GetUserByIdAPI.java b/src/main/java/io/supertokens/webserver/api/core/GetUserByIdAPI.java new file mode 100644 index 000000000..e2562c8fa --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/core/GetUserByIdAPI.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.webserver.api.core; + +import com.google.gson.JsonObject; +import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; +import io.supertokens.Main; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.useridmapping.UserIdType; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class GetUserByIdAPI extends WebserverAPI { + + public GetUserByIdAPI(Main main) { + super(main, ""); + } + + @Override + public String getPath() { + return "/user/id"; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // API is app specific + String userId = InputParser.getQueryParamOrThrowError(req, "userId", false); + + try { + AuthRecipeUserInfo user = null; + + try { + AppIdentifierWithStorageAndUserIdMapping appIdentifierWithStorageAndUserIdMapping = + this.getAppIdentifierWithStorageAndUserIdMappingFromRequest(req, userId, UserIdType.ANY); + // if a userIdMapping exists, pass the superTokensUserId to the getUserUsingId function + if (appIdentifierWithStorageAndUserIdMapping.userIdMapping != null) { + userId = appIdentifierWithStorageAndUserIdMapping.userIdMapping.superTokensUserId; + } + + user = AuthRecipe.getUserById(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, + userId); + + // if a userIdMapping exists, set the userId in the response to the externalUserId + if (user != null) { + UserIdMapping.populateExternalUserIdForUsers(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, new AuthRecipeUserInfo[]{user}); + } + + } catch (UnknownUserIdException e) { + // ignore the error so that the use can remain a null + } + + if (user == null) { + JsonObject result = new JsonObject(); + result.addProperty("status", "UNKNOWN_USER_ID_ERROR"); + super.sendJsonResponse(200, result, resp); + + } else { + JsonObject result = new JsonObject(); + result.addProperty("status", "OK"); + JsonObject userJson = user.toJson(); + + result.add("user", userJson); + super.sendJsonResponse(200, result, resp); + } + + } catch (StorageQueryException | TenantOrAppNotFoundException e) { + throw new ServletException(e); + } + + } +} diff --git a/src/main/java/io/supertokens/webserver/api/core/ListUsersByAccountInfoAPI.java b/src/main/java/io/supertokens/webserver/api/core/ListUsersByAccountInfoAPI.java new file mode 100644 index 000000000..2cc2a7eb5 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/core/ListUsersByAccountInfoAPI.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.webserver.api.core; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.useridmapping.UserIdType; +import io.supertokens.utils.Utils; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class ListUsersByAccountInfoAPI extends WebserverAPI { + + public ListUsersByAccountInfoAPI(Main main) { + super(main, ""); + } + + @Override + public String getPath() { + return "/users/by-accountinfo"; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // API is tenant specific. + String email = InputParser.getQueryParamOrThrowError(req, "email", true); + String phoneNumber = InputParser.getQueryParamOrThrowError(req, "phoneNumber", true); + String thirdPartyId = InputParser.getQueryParamOrThrowError(req, "thirdPartyId", true); + String thirdPartyUserId = InputParser.getQueryParamOrThrowError(req, "thirdPartyUserId", true); + + String doUnionOfAccountInfoStr = InputParser.getQueryParamOrThrowError(req, "doUnionOfAccountInfo", false); + if (!(doUnionOfAccountInfoStr.equals("false") || doUnionOfAccountInfoStr.equals("true"))) { + throw new ServletException(new BadRequestException( + "'doUnionOfAccountInfo' should be either 'true' or 'false'")); + } + boolean doUnionOfAccountInfo = doUnionOfAccountInfoStr.equals("true"); + + if (email != null) { + email = Utils.normaliseEmail(email); + } + if (thirdPartyId != null || thirdPartyUserId != null) { + if (thirdPartyId == null || thirdPartyUserId == null) { + throw new ServletException(new BadRequestException( + "If 'thirdPartyId' is provided, 'thirdPartyUserId' must also be provided, and vice versa")); + } + } + + try { + AppIdentifierWithStorage appIdentifierWithStorage = this.getAppIdentifierWithStorage(req); + AuthRecipeUserInfo[] users = AuthRecipe.getUsersByAccountInfo( + this.getTenantIdentifierWithStorageFromRequest( + req), doUnionOfAccountInfo, email, phoneNumber, thirdPartyId, thirdPartyUserId); + UserIdMapping.populateExternalUserIdForUsers(appIdentifierWithStorage, users); + + JsonObject result = new JsonObject(); + result.addProperty("status", "OK"); + JsonArray usersJson = new JsonArray(); + for (AuthRecipeUserInfo userInfo : users) { + usersJson.add(userInfo.toJson()); + } + + result.add("users", usersJson); + super.sendJsonResponse(200, result, resp); + + } catch (StorageQueryException | TenantOrAppNotFoundException e) { + throw new ServletException(e); + } + + } +} diff --git a/src/main/java/io/supertokens/webserver/api/core/UsersAPI.java b/src/main/java/io/supertokens/webserver/api/core/UsersAPI.java index 9184eec20..4e7e2120c 100644 --- a/src/main/java/io/supertokens/webserver/api/core/UsersAPI.java +++ b/src/main/java/io/supertokens/webserver/api/core/UsersAPI.java @@ -16,13 +16,16 @@ package io.supertokens.webserver.api.core; -import com.google.gson.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.authRecipe.UserPaginationContainer; import io.supertokens.authRecipe.UserPaginationToken; import io.supertokens.output.Logging; import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; @@ -165,25 +168,23 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO limit, timeJoinedOrder, paginationToken, recipeIdsEnumBuilder.build().toArray(RECIPE_ID[]::new), searchTags); - ArrayList userIds = new ArrayList<>(); - for (int i = 0; i < users.users.length; i++) { - userIds.add(users.users[i].user.id); - } - HashMap userIdMapping = UserIdMapping.getUserIdMappingForSuperTokensUserIds( - tenantIdentifierWithStorage, userIds); - if (!userIdMapping.isEmpty()) { - for (int i = 0; i < users.users.length; i++) { - String externalId = userIdMapping.get(userIds.get(i)); - if (externalId != null) { - users.users[i].user.id = externalId; - } - } - } + UserIdMapping.populateExternalUserIdForUsers(tenantIdentifierWithStorage, users.users); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); - JsonArray usersJson = new JsonParser().parse(new Gson().toJson(users.users)).getAsJsonArray(); + JsonArray usersJson = new JsonArray(); + for (AuthRecipeUserInfo user : users.users) { + if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) { + JsonObject jsonObj = new JsonObject(); + jsonObj.addProperty("recipeId", user.loginMethods[0].recipeId.toString()); + JsonObject userJson = user.toJsonWithoutAccountLinking(); + jsonObj.add("user", userJson); + usersJson.add(jsonObj); + } else { + usersJson.add(user.toJson()); + } + } if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { for (JsonElement user : usersJson) { diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/ConsumeResetPasswordAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/ConsumeResetPasswordAPI.java new file mode 100644 index 000000000..362ce9020 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/ConsumeResetPasswordAPI.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2020, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.webserver.api.emailpassword; + +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.ResetPasswordInvalidTokenException; +import io.supertokens.output.Logging; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.useridmapping.UserIdType; +import io.supertokens.utils.Utils; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + +public class ConsumeResetPasswordAPI extends WebserverAPI { + private static final long serialVersionUID = -7529428297450682549L; + + public ConsumeResetPasswordAPI(Main main) { + super(main, RECIPE_ID.EMAIL_PASSWORD.toString()); + } + + @Override + public String getPath() { + return "/recipe/user/password/reset/token/consume"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // API is tenant specific + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String token = InputParser.parseStringOrThrowError(input, "token", false); + assert token != null; + + TenantIdentifierWithStorage tenantIdentifierWithStorage = null; + try { + tenantIdentifierWithStorage = getTenantIdentifierWithStorageFromRequest(req); + } catch (TenantOrAppNotFoundException e) { + throw new ServletException(e); + } + + try { + EmailPassword.ConsumeResetPasswordTokenResult result = EmailPassword.consumeResetPasswordToken( + tenantIdentifierWithStorage, token); + + io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = UserIdMapping.getUserIdMapping( + tenantIdentifierWithStorage.toAppIdentifierWithStorage(), result.userId, UserIdType.SUPERTOKENS); + + // if userIdMapping exists, pass the externalUserId to the response + if (userIdMapping != null) { + result.userId = userIdMapping.externalUserId; + } + + JsonObject resultJson = new JsonObject(); + resultJson.addProperty("status", "OK"); + resultJson.addProperty("userId", result.userId); + resultJson.addProperty("email", result.email); + + super.sendJsonResponse(200, resultJson, resp); + + } catch (ResetPasswordInvalidTokenException e) { + Logging.debug(main, tenantIdentifierWithStorage, Utils.exceptionStacktraceToString(e)); + JsonObject result = new JsonObject(); + result.addProperty("status", "RESET_PASSWORD_INVALID_TOKEN_ERROR"); + super.sendJsonResponse(200, result, resp); + + } catch (StorageQueryException | NoSuchAlgorithmException | StorageTransactionLogicException | + TenantOrAppNotFoundException e) { + throw new ServletException(e); + } + + } + +} diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/GeneratePasswordResetTokenAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/GeneratePasswordResetTokenAPI.java index b5d97500e..24f55a679 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/GeneratePasswordResetTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/GeneratePasswordResetTokenAPI.java @@ -18,6 +18,7 @@ import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.TenantIdentifierWithStorageAndUserIdMapping; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.output.Logging; @@ -25,10 +26,9 @@ import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.TenantIdentifierWithStorageAndUserIdMapping; import io.supertokens.useridmapping.UserIdType; +import io.supertokens.utils.SemVer; import io.supertokens.utils.Utils; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; @@ -58,7 +58,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I // API is tenant specific JsonObject input = InputParser.parseJsonObjectOrThrowError(req); String userId = InputParser.parseStringOrThrowError(input, "userId", false); - assert userId != null; // logic according to https://github.com/supertokens/supertokens-core/issues/106 TenantIdentifier tenantIdentifier = null; @@ -76,7 +75,15 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I userId = tenantIdentifierStorageAndMapping.userIdMapping.superTokensUserId; } - String token = EmailPassword.generatePasswordResetToken(tenantIdentifierStorageAndMapping.tenantIdentifierWithStorage, super.main, userId); + String token; + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { + String email = InputParser.parseStringOrThrowError(input, "email", false); + token = EmailPassword.generatePasswordResetToken( + tenantIdentifierStorageAndMapping.tenantIdentifierWithStorage, super.main, userId, email); + } else { + token = EmailPassword.generatePasswordResetTokenBeforeCdi4_0( + tenantIdentifierStorageAndMapping.tenantIdentifierWithStorage, super.main, userId); + } JsonObject result = new JsonObject(); result.addProperty("status", "OK"); @@ -89,7 +96,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I result.addProperty("status", "UNKNOWN_USER_ID_ERROR"); super.sendJsonResponse(200, result, resp); - } catch (StorageQueryException | BadPermissionException | NoSuchAlgorithmException | InvalidKeySpecException | TenantOrAppNotFoundException e) { + } catch (StorageQueryException | BadPermissionException | NoSuchAlgorithmException | InvalidKeySpecException | + TenantOrAppNotFoundException | BadRequestException e) { throw new ServletException(e); } diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/ImportUserWithPasswordHashAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/ImportUserWithPasswordHashAPI.java index cc71246f2..f1d9a7a9a 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/ImportUserWithPasswordHashAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/ImportUserWithPasswordHashAPI.java @@ -16,18 +16,19 @@ package io.supertokens.webserver.api.emailpassword; -import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import io.supertokens.Main; import io.supertokens.config.CoreConfig; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.emailpassword.exceptions.UnsupportedPasswordHashingFormatException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.useridmapping.UserIdMapping; import io.supertokens.utils.SemVer; import io.supertokens.utils.Utils; import io.supertokens.webserver.InputParser; @@ -93,12 +94,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } try { + TenantIdentifierWithStorage tenant = this.getTenantIdentifierWithStorageFromRequest(req); EmailPassword.ImportUserResponse importUserResponse = EmailPassword.importUserWithPasswordHash( - this.getTenantIdentifierWithStorageFromRequest(req), main, email, + tenant, main, email, passwordHash, passwordHashingAlgorithm); + UserIdMapping.populateExternalUserIdForUsers(getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{importUserResponse.user}); JsonObject response = new JsonObject(); response.addProperty("status", "OK"); - JsonObject userJson = new JsonParser().parse(new Gson().toJson(importUserResponse.user)).getAsJsonObject(); + JsonObject userJson = + getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? importUserResponse.user.toJson() : + importUserResponse.user.toJsonWithoutAccountLinking(); if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { userJson.remove("tenantIds"); @@ -107,7 +112,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I response.add("user", userJson); response.addProperty("didUserAlreadyExist", importUserResponse.didUserAlreadyExist); super.sendJsonResponse(200, response, resp); - } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | BadPermissionException e) { + } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | + BadPermissionException e) { throw new ServletException(e); } catch (UnsupportedPasswordHashingFormatException e) { throw new ServletException(new WebserverAPI.BadRequestException(e.getMessage())); diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/ResetPasswordAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/ResetPasswordAPI.java index a46b58905..05d354676 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/ResetPasswordAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/ResetPasswordAPI.java @@ -24,7 +24,6 @@ import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; -import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.useridmapping.UserIdMapping; @@ -40,6 +39,7 @@ import java.io.IOException; import java.security.NoSuchAlgorithmException; +@Deprecated public class ResetPasswordAPI extends WebserverAPI { private static final long serialVersionUID = -7529428297450682549L; @@ -94,8 +94,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject result = new JsonObject(); result.addProperty("status", "OK"); - if (!(super.getVersionFromRequest(req).equals(SemVer.v2_7) || super.getVersionFromRequest(req).equals(SemVer.v2_8) - || super.getVersionFromRequest(req).equals(SemVer.v2_9) || super.getVersionFromRequest(req).equals(SemVer.v2_10) + if (!(super.getVersionFromRequest(req).equals(SemVer.v2_7) || + super.getVersionFromRequest(req).equals(SemVer.v2_8) + || super.getVersionFromRequest(req).equals(SemVer.v2_9) || + super.getVersionFromRequest(req).equals(SemVer.v2_10) || super.getVersionFromRequest(req).equals(SemVer.v2_11))) { // >= 2.12 result.addProperty("userId", userId); @@ -109,7 +111,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I result.addProperty("status", "RESET_PASSWORD_INVALID_TOKEN_ERROR"); super.sendJsonResponse(200, result, resp); - } catch (StorageQueryException | NoSuchAlgorithmException | StorageTransactionLogicException | TenantOrAppNotFoundException e) { + } catch (StorageQueryException | NoSuchAlgorithmException | StorageTransactionLogicException | + TenantOrAppNotFoundException e) { throw new ServletException(e); } diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java index 8f2568d0b..cf57898cd 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java @@ -16,10 +16,7 @@ package io.supertokens.webserver.api.emailpassword; -import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - import io.supertokens.ActiveUsers; import io.supertokens.Main; import io.supertokens.emailpassword.EmailPassword; @@ -27,7 +24,8 @@ import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.output.Logging; import io.supertokens.pluginInterface.RECIPE_ID; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; @@ -77,25 +75,30 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } try { - UserInfo user = EmailPassword.signIn(tenantIdentifierWithStorage, super.main, normalisedEmail, password); - - ActiveUsers.updateLastActive(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main, user.id); // use the internal user id - - // if a userIdMapping exists, pass the externalUserId to the response - UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping( - tenantIdentifierWithStorage.toAppIdentifierWithStorage(), user.id, UserIdType.SUPERTOKENS); + AuthRecipeUserInfo user = EmailPassword.signIn(tenantIdentifierWithStorage, super.main, normalisedEmail, + password); + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(tenantIdentifierWithStorage, new AuthRecipeUserInfo[]{user}); - if (userIdMapping != null) { - user.id = userIdMapping.externalUserId; - } + ActiveUsers.updateLastActive(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main, + user.getSupertokensUserId()); // use the internal user id JsonObject result = new JsonObject(); result.addProperty("status", "OK"); - JsonObject userJson = new JsonParser().parse(new Gson().toJson(user)).getAsJsonObject(); + JsonObject userJson = getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? user.toJson() : + user.toJsonWithoutAccountLinking(); if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { userJson.remove("tenantIds"); } result.add("user", userJson); + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { + for (LoginMethod loginMethod : user.loginMethods) { + if (loginMethod.recipeId.equals(RECIPE_ID.EMAIL_PASSWORD) && normalisedEmail.equals(loginMethod.email)) { + result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId()); + break; + } + } + } + super.sendJsonResponse(200, result, resp); } catch (WrongCredentialsException e) { diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java index 8c184b021..bb915fadb 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java @@ -16,20 +16,18 @@ package io.supertokens.webserver.api.emailpassword; -import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - import io.supertokens.ActiveUsers; import io.supertokens.Main; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.output.Logging; import io.supertokens.pluginInterface.RECIPE_ID; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.utils.SemVer; import io.supertokens.utils.Utils; @@ -79,21 +77,27 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } try { - UserInfo user = EmailPassword.signUp(this.getTenantIdentifierWithStorageFromRequest(req), super.main, normalisedEmail, password); + TenantIdentifierWithStorage tenant = this.getTenantIdentifierWithStorageFromRequest(req); + AuthRecipeUserInfo user = EmailPassword.signUp(tenant, super.main, normalisedEmail, password); - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, user.id); + ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); - JsonObject userJson = new JsonParser().parse(new Gson().toJson(user)).getAsJsonObject(); + user.setExternalUserId(null); + JsonObject userJson = + getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? user.toJson() : + user.toJsonWithoutAccountLinking(); if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { userJson.remove("tenantIds"); } result.add("user", userJson); + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { + result.addProperty("recipeUserId", user.getSupertokensOrExternalUserId()); + } super.sendJsonResponse(200, result, resp); - } catch (DuplicateEmailException e) { Logging.debug(main, tenantIdentifier, Utils.exceptionStacktraceToString(e)); JsonObject result = new JsonObject(); diff --git a/src/main/java/io/supertokens/webserver/api/emailpassword/UserAPI.java b/src/main/java/io/supertokens/webserver/api/emailpassword/UserAPI.java index d3503ed2c..7f992f6a5 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/UserAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/UserAPI.java @@ -16,14 +16,14 @@ package io.supertokens.webserver.api.emailpassword; -import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; import io.supertokens.Main; import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.output.Logging; import io.supertokens.pluginInterface.RECIPE_ID; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -31,7 +31,6 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; import io.supertokens.useridmapping.UserIdMapping; import io.supertokens.useridmapping.UserIdType; import io.supertokens.utils.SemVer; @@ -57,6 +56,7 @@ public String getPath() { return "/recipe/user"; } + @Deprecated @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { // API is tenant specific for get by Email and app specific for get by UserId @@ -75,7 +75,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO try { // API is app specific for get by UserId - UserInfo user = null; + AuthRecipeUserInfo user = null; try { if (userId != null) { @@ -89,27 +89,22 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO user = EmailPassword.getUserUsingId( appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, userId); - - // if the userIdMapping exists set the userId in the response to the externalUserId - if (user != null && appIdentifierWithStorageAndUserIdMapping.userIdMapping != null) { - user.id = appIdentifierWithStorageAndUserIdMapping.userIdMapping.externalUserId; - } + if (user != null) { + UserIdMapping.populateExternalUserIdForUsers(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, new AuthRecipeUserInfo[]{user}); + } } else { // API is tenant specific for get by Email // Query by email String normalisedEmail = Utils.normaliseEmail(email); - TenantIdentifierWithStorage tenantIdentifierWithStorage = this.getTenantIdentifierWithStorageFromRequest(req); + TenantIdentifierWithStorage tenantIdentifierWithStorage = + this.getTenantIdentifierWithStorageFromRequest( + req); user = EmailPassword.getUserUsingEmail(tenantIdentifierWithStorage, normalisedEmail); // if a userIdMapping exists, set the userId in the response to the externalUserId if (user != null) { - io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = - UserIdMapping.getUserIdMapping( - getAppIdentifierWithStorage(req), user.id, UserIdType.SUPERTOKENS); - if (userIdMapping != null) { - user.id = userIdMapping.externalUserId; - } + UserIdMapping.populateExternalUserIdForUsers(tenantIdentifierWithStorage, new AuthRecipeUserInfo[]{user}); } } } catch (UnknownUserIdException e) { @@ -124,7 +119,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO } else { JsonObject result = new JsonObject(); result.addProperty("status", "OK"); - JsonObject userJson = new JsonParser().parse(new Gson().toJson(user)).getAsJsonObject(); + JsonObject userJson = + getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? user.toJson() : + user.toJsonWithoutAccountLinking(); if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { userJson.remove("tenantIds"); @@ -144,7 +141,13 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { // API is app specific JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - String userId = InputParser.parseStringOrThrowError(input, "userId", false); + String userId; + + if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) { + userId = InputParser.parseStringOrThrowError(input, "userId", false); + } else { + userId = InputParser.parseStringOrThrowError(input, "recipeUserId", false); + } String email = InputParser.parseStringOrThrowError(input, "email", true); String password = InputParser.parseStringOrThrowError(input, "password", true); @@ -192,6 +195,12 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject result = new JsonObject(); result.addProperty("status", "EMAIL_ALREADY_EXISTS_ERROR"); super.sendJsonResponse(200, result, resp); + } catch (EmailChangeNotAllowedException e) { + Logging.debug(main, appIdentifier.getAsPublicTenantIdentifier(), Utils.exceptionStacktraceToString(e)); + JsonObject result = new JsonObject(); + result.addProperty("status", "EMAIL_CHANGE_NOT_ALLOWED_ERROR"); + result.addProperty("reason", "New email is associated with another primary user ID"); + super.sendJsonResponse(200, result, resp); } } } diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java index d53c2e7e8..739d182bd 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java @@ -21,6 +21,9 @@ import io.supertokens.Main; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithEmailAlreadyExistsException; +import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithPhoneNumberAlreadyExistsException; +import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; @@ -29,6 +32,7 @@ import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.useridmapping.UserIdType; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -53,7 +57,13 @@ public String getPath() { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - String userId = InputParser.parseStringOrThrowError(input, "userId", false); + String userId; + + if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) { + userId = InputParser.parseStringOrThrowError(input, "userId", false); + } else { + userId = InputParser.parseStringOrThrowError(input, "recipeUserId", false); + } // normalize userId userId = userId.trim(); @@ -99,6 +109,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject result = new JsonObject(); result.addProperty("status", "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"); super.sendJsonResponse(200, result, resp); + + } catch (AnotherPrimaryUserWithEmailAlreadyExistsException | AnotherPrimaryUserWithPhoneNumberAlreadyExistsException | + AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException e) { + JsonObject result = new JsonObject(); + result.addProperty("status", "ASSOCIATION_NOT_ALLOWED_ERROR"); + result.addProperty("reason", e.getMessage()); + super.sendJsonResponse(200, result, resp); } } } diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java b/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java index e97f94f64..a37c3a595 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java @@ -26,6 +26,7 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.useridmapping.UserIdType; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -49,7 +50,13 @@ public String getPath() { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - String userId = InputParser.parseStringOrThrowError(input, "userId", false); + String userId; + + if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) { + userId = InputParser.parseStringOrThrowError(input, "userId", false); + } else { + userId = InputParser.parseStringOrThrowError(input, "recipeUserId", false); + } // normalize userId userId = userId.trim(); diff --git a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java index 5e654c22a..6329a81d2 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java @@ -16,9 +16,7 @@ package io.supertokens.webserver.api.passwordless; -import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import io.supertokens.ActiveUsers; import io.supertokens.Main; import io.supertokens.multitenancy.exception.BadPermissionException; @@ -26,6 +24,8 @@ import io.supertokens.passwordless.Passwordless.ConsumeCodeResponse; import io.supertokens.passwordless.exceptions.*; import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; @@ -41,6 +41,7 @@ import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Objects; public class ConsumeCodeAPI extends WebserverAPI { @@ -85,20 +86,18 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I ConsumeCodeResponse consumeCodeResponse = Passwordless.consumeCode( this.getTenantIdentifierWithStorageFromRequest(req), main, deviceId, deviceIdHash, - userInputCode, linkCode); - - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, consumeCodeResponse.user.id); - - UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping( - this.getAppIdentifierWithStorage(req), - consumeCodeResponse.user.id, UserIdType.ANY); - if (userIdMapping != null) { - consumeCodeResponse.user.id = userIdMapping.externalUserId; - } + userInputCode, linkCode, + // From CDI version 4.0 onwards, the email verification will be set + getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)); + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{consumeCodeResponse.user}); + + ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, consumeCodeResponse.user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); - JsonObject userJson = new JsonParser().parse(new Gson().toJson(consumeCodeResponse.user)).getAsJsonObject(); + JsonObject userJson = + getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? consumeCodeResponse.user.toJson() : + consumeCodeResponse.user.toJsonWithoutAccountLinking(); if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { userJson.remove("tenantIds"); @@ -106,6 +105,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I result.addProperty("createdNewUser", consumeCodeResponse.createdNewUser); result.add("user", userJson); + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { + for (LoginMethod loginMethod : consumeCodeResponse.user.loginMethods) { + if (loginMethod.recipeId.equals(RECIPE_ID.PASSWORDLESS) + && (consumeCodeResponse.email == null || Objects.equals(loginMethod.email, consumeCodeResponse.email)) + && (consumeCodeResponse.phoneNumber == null || Objects.equals(loginMethod.phoneNumber, consumeCodeResponse.phoneNumber))) { + result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId()); + break; + } + } + } super.sendJsonResponse(200, result, resp); } catch (RestartFlowException ex) { @@ -127,7 +136,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I super.sendJsonResponse(200, result, resp); } catch (DeviceIdHashMismatchException ex) { throw new ServletException(new BadRequestException("preAuthSessionId and deviceId doesn't match")); - } catch (StorageTransactionLogicException | StorageQueryException | NoSuchAlgorithmException | InvalidKeyException | TenantOrAppNotFoundException | BadPermissionException e) { + } catch (StorageTransactionLogicException | StorageQueryException | NoSuchAlgorithmException | + InvalidKeyException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); } catch (Base64EncodingException ex) { throw new ServletException(new BadRequestException("Input encoding error in " + ex.source)); diff --git a/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java b/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java index ccd7a7651..253a3f81a 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java @@ -16,22 +16,21 @@ package io.supertokens.webserver.api.passwordless; -import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; import io.supertokens.Main; -import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.passwordless.Passwordless; import io.supertokens.passwordless.Passwordless.FieldUpdate; +import io.supertokens.passwordless.exceptions.PhoneNumberChangeNotAllowedException; import io.supertokens.passwordless.exceptions.UserWithoutContactInfoException; import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; -import io.supertokens.pluginInterface.passwordless.UserInfo; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException; -import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import io.supertokens.useridmapping.UserIdType; import io.supertokens.utils.SemVer; import io.supertokens.utils.Utils; @@ -58,6 +57,7 @@ public String getPath() { return "/recipe/user"; } + @Deprecated @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { // API is tenant specific for get by email or phone @@ -73,7 +73,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO } try { - UserInfo user; + AuthRecipeUserInfo user; if (userId != null) { try { AppIdentifierWithStorageAndUserIdMapping appIdentifierWithStorageAndUserIdMapping = @@ -85,8 +85,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO userId); // if the userIdMapping exists set the userId in the response to the externalUserId - if (user != null && appIdentifierWithStorageAndUserIdMapping.userIdMapping != null) { - user.id = appIdentifierWithStorageAndUserIdMapping.userIdMapping.externalUserId; + if (user != null) { + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, new AuthRecipeUserInfo[]{user}); } } catch (UnknownUserIdException e) { user = null; @@ -95,22 +95,13 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO email = Utils.normaliseEmail(email); user = Passwordless.getUserByEmail(this.getTenantIdentifierWithStorageFromRequest(req), email); if (user != null) { - UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping( - this.getAppIdentifierWithStorage(req), - user.id, UserIdType.SUPERTOKENS); - if (userIdMapping != null) { - user.id = userIdMapping.externalUserId; - } + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{user}); } } else { - user = Passwordless.getUserByPhoneNumber(this.getTenantIdentifierWithStorageFromRequest(req), phoneNumber); + user = Passwordless.getUserByPhoneNumber(this.getTenantIdentifierWithStorageFromRequest(req), + phoneNumber); if (user != null) { - UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping( - this.getAppIdentifierWithStorage(req), - user.id, UserIdType.SUPERTOKENS); - if (userIdMapping != null) { - user.id = userIdMapping.externalUserId; - } + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{user}); } } @@ -123,7 +114,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject result = new JsonObject(); result.addProperty("status", "OK"); - JsonObject userJson = new JsonParser().parse(new Gson().toJson(user)).getAsJsonObject(); + JsonObject userJson = + getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? user.toJson() : + user.toJsonWithoutAccountLinking(); if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { userJson.remove("tenantIds"); @@ -142,15 +135,21 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO // API is app specific // logic based on: https://app.code2flow.com/TXloWHJOwWKg JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - String userId = InputParser.parseStringOrThrowError(input, "userId", false); + String userId; + + if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) { + userId = InputParser.parseStringOrThrowError(input, "userId", false); + } else { + userId = InputParser.parseStringOrThrowError(input, "recipeUserId", false); + } FieldUpdate emailUpdate = !input.has("email") ? null : new FieldUpdate(input.get("email").isJsonNull() ? null - : Utils.normaliseEmail(InputParser.parseStringOrThrowError(input, "email", false))); + : Utils.normaliseEmail(InputParser.parseStringOrThrowError(input, "email", false))); FieldUpdate phoneNumberUpdate = !input.has("phoneNumber") ? null : new FieldUpdate(input.get("phoneNumber").isJsonNull() ? null - : InputParser.parseStringOrThrowError(input, "phoneNumber", false)); + : InputParser.parseStringOrThrowError(input, "phoneNumber", false)); try { AppIdentifierWithStorageAndUserIdMapping appIdentifierWithStorageAndUserIdMapping = @@ -183,6 +182,16 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO } catch (UserWithoutContactInfoException e) { throw new ServletException( new BadRequestException("You cannot clear both email and phone number of a user")); + } catch (EmailChangeNotAllowedException e) { + JsonObject result = new JsonObject(); + result.addProperty("status", "EMAIL_CHANGE_NOT_ALLOWED_ERROR"); + result.addProperty("reason", "New email is associated with another primary user ID"); + super.sendJsonResponse(200, result, resp); + } catch (PhoneNumberChangeNotAllowedException e) { + JsonObject result = new JsonObject(); + result.addProperty("status", "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"); + result.addProperty("reason", "New phone number is associated with another primary user ID"); + super.sendJsonResponse(200, result, resp); } } } diff --git a/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java b/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java index 33e92983f..ee92da0bd 100644 --- a/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java @@ -109,24 +109,31 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (version.lesserThan(SemVer.v3_0)) { result.get("session").getAsJsonObject().remove("tenantId"); } + if (version.lesserThan(SemVer.v4_0)) { + result.get("session").getAsJsonObject().remove("recipeUserId"); + } result.addProperty("status", "OK"); super.sendJsonResponse(200, result, resp); - } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | UnsupportedJWTSigningAlgorithmException e) { + } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | + UnsupportedJWTSigningAlgorithmException e) { throw new ServletException(e); } catch (AccessTokenPayloadError | UnauthorisedException e) { - Logging.debug(main, appIdentifierWithStorage.getAsPublicTenantIdentifier(), Utils.exceptionStacktraceToString(e)); + Logging.debug(main, appIdentifierWithStorage.getAsPublicTenantIdentifier(), + Utils.exceptionStacktraceToString(e)); JsonObject reply = new JsonObject(); reply.addProperty("status", "UNAUTHORISED"); reply.addProperty("message", e.getMessage()); super.sendJsonResponse(200, reply, resp); } catch (TokenTheftDetectedException e) { - Logging.debug(main, appIdentifierWithStorage.getAsPublicTenantIdentifier(), Utils.exceptionStacktraceToString(e)); + Logging.debug(main, appIdentifierWithStorage.getAsPublicTenantIdentifier(), + Utils.exceptionStacktraceToString(e)); JsonObject reply = new JsonObject(); reply.addProperty("status", "TOKEN_THEFT_DETECTED"); JsonObject session = new JsonObject(); session.addProperty("handle", e.sessionHandle); - session.addProperty("userId", e.userId); + session.addProperty("userId", e.primaryUserId); + session.addProperty("recipeUserId", e.recipeUserId); reply.add("session", session); super.sendJsonResponse(200, reply, resp); diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java index 0ea0b0be9..4263285c2 100644 --- a/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java @@ -34,7 +34,6 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.session.SessionInfo; -import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import io.supertokens.session.Session; import io.supertokens.session.accessToken.AccessToken; import io.supertokens.session.info.SessionInformationHolder; @@ -108,8 +107,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping( - this.getAppIdentifierWithStorage(req), - sessionInfo.session.userId, UserIdType.ANY); + this.getAppIdentifierWithStorage(req), + sessionInfo.session.userId, UserIdType.ANY); if (userIdMapping != null) { ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, userIdMapping.superTokensUserId); @@ -126,6 +125,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { result.get("session").getAsJsonObject().remove("tenantId"); } + if (version.lesserThan(SemVer.v4_0)) { + result.get("session").getAsJsonObject().remove("recipeUserId"); + } result.addProperty("status", "OK"); @@ -139,7 +141,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I super.sendJsonResponse(200, result, resp); } catch (AccessTokenPayloadError e) { throw new ServletException(new BadRequestException(e.getMessage())); - } catch (NoSuchAlgorithmException | StorageQueryException | InvalidKeyException | InvalidKeySpecException | StorageTransactionLogicException | SignatureException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException | NoSuchPaddingException | TenantOrAppNotFoundException | UnsupportedJWTSigningAlgorithmException e) { + } catch (NoSuchAlgorithmException | StorageQueryException | InvalidKeyException | InvalidKeySpecException | + StorageTransactionLogicException | SignatureException | IllegalBlockSizeException | + BadPaddingException | InvalidAlgorithmParameterException | NoSuchPaddingException | + TenantOrAppNotFoundException | UnsupportedJWTSigningAlgorithmException e) { throw new ServletException(e); } } @@ -153,7 +158,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO TenantIdentifierWithStorage tenantIdentifierWithStorage = null; try { AppIdentifierWithStorage appIdentifier = getAppIdentifierWithStorage(req); - TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), Session.getTenantIdFromSessionHandle(sessionHandle)); + TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifier.getConnectionUriDomain(), + appIdentifier.getAppId(), Session.getTenantIdFromSessionHandle(sessionHandle)); tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); } catch (TenantOrAppNotFoundException e) { throw new ServletException(e); @@ -171,6 +177,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v3_0)) { result.addProperty("tenantId", tenantIdentifierWithStorage.getTenantId()); } + if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) { + result.remove("recipeUserId"); + } super.sendJsonResponse(200, result, resp); diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionRegenerateAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionRegenerateAPI.java index 471518e3e..0f2843e65 100644 --- a/src/main/java/io/supertokens/webserver/api/session/SessionRegenerateAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/SessionRegenerateAPI.java @@ -86,6 +86,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { result.get("session").getAsJsonObject().remove("tenantId"); } + if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) { + result.get("session").getAsJsonObject().remove("recipeUserId"); + } result.addProperty("status", "OK"); super.sendJsonResponse(200, result, resp); diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java index 8446e20a9..127e0b5fc 100644 --- a/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java @@ -80,22 +80,33 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I Boolean revokeAcrossAllTenants = InputParser.parseBooleanOrThrowError(input, "revokeAcrossAllTenants", true); if (userId == null && revokeAcrossAllTenants != null) { - throw new ServletException(new BadRequestException("Invalid JSON input - revokeAcrossAllTenants can only be set if userId is set")); + throw new ServletException(new BadRequestException( + "Invalid JSON input - revokeAcrossAllTenants can only be set if userId is set")); } - if (revokeAcrossAllTenants == null) { revokeAcrossAllTenants = true; } + Boolean revokeSessionsForLinkedAccounts = InputParser.parseBooleanOrThrowError(input, + "revokeSessionsForLinkedAccounts", true); + if (userId == null && revokeSessionsForLinkedAccounts != null) { + throw new ServletException(new BadRequestException( + "Invalid JSON input - revokeSessionsForLinkedAccounts can only be set if userId is set")); + } + if (revokeSessionsForLinkedAccounts == null) { + revokeSessionsForLinkedAccounts = true; + } + if (userId != null) { try { String[] sessionHandlesRevoked; if (revokeAcrossAllTenants) { sessionHandlesRevoked = Session.revokeAllSessionsForUser( - main, this.getAppIdentifierWithStorage(req), userId); + main, this.getAppIdentifierWithStorage(req), userId, revokeSessionsForLinkedAccounts); } else { sessionHandlesRevoked = Session.revokeAllSessionsForUser( - main, this.getTenantIdentifierWithStorageFromRequest(req), userId); + main, this.getTenantIdentifierWithStorageFromRequest(req), userId, + revokeSessionsForLinkedAccounts); } if (StorageLayer.getStorage(this.getTenantIdentifierWithStorageFromRequest(req), main).getType() == diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionUserAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionUserAPI.java index 443594c7c..e31c32ee1 100644 --- a/src/main/java/io/supertokens/webserver/api/session/SessionUserAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/SessionUserAPI.java @@ -20,9 +20,9 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import io.supertokens.Main; -import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.session.Session; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; @@ -58,14 +58,21 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO fetchAcrossAllTenants = fetchAcrossAllTenantsString.toLowerCase().equals("true"); } + String fetchSessionsForAllLinkedAccountsString = InputParser.getQueryParamOrThrowError(req, + "fetchSessionsForAllLinkedAccounts", true); + boolean fetchSessionsForAllLinkedAccounts = true; + if (fetchSessionsForAllLinkedAccountsString != null) { + fetchSessionsForAllLinkedAccounts = fetchSessionsForAllLinkedAccountsString.toLowerCase().equals("true"); + } + try { String[] sessionHandles; if (fetchAcrossAllTenants) { sessionHandles = Session.getAllNonExpiredSessionHandlesForUser( - main, this.getAppIdentifierWithStorage(req), userId); + main, this.getAppIdentifierWithStorage(req), userId, fetchSessionsForAllLinkedAccounts); } else { sessionHandles = Session.getAllNonExpiredSessionHandlesForUser( - this.getTenantIdentifierWithStorageFromRequest(req), userId); + this.getTenantIdentifierWithStorageFromRequest(req), userId, fetchSessionsForAllLinkedAccounts); } JsonObject result = new JsonObject(); diff --git a/src/main/java/io/supertokens/webserver/api/session/VerifySessionAPI.java b/src/main/java/io/supertokens/webserver/api/session/VerifySessionAPI.java index a1da647bc..8b3fd7d84 100644 --- a/src/main/java/io/supertokens/webserver/api/session/VerifySessionAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/VerifySessionAPI.java @@ -95,7 +95,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (!super.getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v2_21)) { result.addProperty("jwtSigningPublicKey", - new Utils.PubPriKey(SigningKeys.getInstance(appIdentifier, main).getLatestIssuedDynamicKey().value).publicKey); + new Utils.PubPriKey(SigningKeys.getInstance(appIdentifier, main) + .getLatestIssuedDynamicKey().value).publicKey); result.addProperty("jwtSigningPublicKeyExpiryTime", SigningKeys.getInstance(appIdentifier, main).getDynamicSigningKeyExpiryTime()); @@ -106,9 +107,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { result.get("session").getAsJsonObject().remove("tenantId"); } + if (getVersionFromRequest(req).lesserThan(SemVer.v4_0)) { + result.get("session").getAsJsonObject().remove("recipeUserId"); + } super.sendJsonResponse(200, result, resp); - } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | UnsupportedJWTSigningAlgorithmException e) { + } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | + UnsupportedJWTSigningAlgorithmException e) { throw new ServletException(e); } catch (AccessTokenPayloadError e) { throw new ServletException(new BadRequestException(e.getMessage())); @@ -136,7 +141,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I reply.addProperty("message", e.getMessage()); super.sendJsonResponse(200, reply, resp); - } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | UnsupportedJWTSigningAlgorithmException e2) { + } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException | + UnsupportedJWTSigningAlgorithmException e2) { throw new ServletException(e2); } } diff --git a/src/main/java/io/supertokens/webserver/api/thirdparty/GetUsersByEmailAPI.java b/src/main/java/io/supertokens/webserver/api/thirdparty/GetUsersByEmailAPI.java index 73350b50a..391db2c70 100644 --- a/src/main/java/io/supertokens/webserver/api/thirdparty/GetUsersByEmailAPI.java +++ b/src/main/java/io/supertokens/webserver/api/thirdparty/GetUsersByEmailAPI.java @@ -16,14 +16,16 @@ package io.supertokens.webserver.api.thirdparty; -import com.google.gson.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import io.supertokens.Main; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.pluginInterface.RECIPE_ID; -import io.supertokens.pluginInterface.exceptions.StorageQueryException; -import io.supertokens.pluginInterface.thirdparty.UserInfo; import io.supertokens.thirdparty.ThirdParty; import io.supertokens.useridmapping.UserIdMapping; import io.supertokens.useridmapping.UserIdType; @@ -37,6 +39,7 @@ import java.io.IOException; +@Deprecated public class GetUsersByEmailAPI extends WebserverAPI { private static final long serialVersionUID = -4413719941975228004L; @@ -53,27 +56,22 @@ public String getPath() { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { // this API is tenant specific try { - TenantIdentifierWithStorage tenantIdentifierWithStorage = this.getTenantIdentifierWithStorageFromRequest(req); + TenantIdentifierWithStorage tenantIdentifierWithStorage = this.getTenantIdentifierWithStorageFromRequest( + req); AppIdentifierWithStorage appIdentifierWithStorage = this.getAppIdentifierWithStorage(req); String email = InputParser.getQueryParamOrThrowError(req, "email", false); email = Utils.normaliseEmail(email); - UserInfo[] users = ThirdParty.getUsersByEmail(tenantIdentifierWithStorage, email); - - // return the externalUserId if a mapping exists for a user - for (int i = 0; i < users.length; i++) { - // we intentionally do not use the function that accepts an array of user IDs to get the mapping cause - // this is simpler to use, and cause there shouldn't be that many userIds per email anyway - io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = UserIdMapping - .getUserIdMapping(appIdentifierWithStorage, users[i].id, UserIdType.SUPERTOKENS); - if (userIdMapping != null) { - users[i].id = userIdMapping.externalUserId; - } - } + AuthRecipeUserInfo[] users = ThirdParty.getUsersByEmail(tenantIdentifierWithStorage, email); + UserIdMapping.populateExternalUserIdForUsers(tenantIdentifierWithStorage, users); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); - JsonArray usersJson = new JsonParser().parse(new Gson().toJson(users)).getAsJsonArray(); + JsonArray usersJson = new JsonArray(); + for (AuthRecipeUserInfo userInfo : users) { + usersJson.add(getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? userInfo.toJson() : + userInfo.toJsonWithoutAccountLinking()); + } if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { for (JsonElement user : usersJson) { diff --git a/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java b/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java index 61e612278..06042eb23 100644 --- a/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java @@ -16,13 +16,14 @@ package io.supertokens.webserver.api.thirdparty; -import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import io.supertokens.ActiveUsers; import io.supertokens.Main; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.thirdparty.ThirdParty; @@ -37,6 +38,7 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Objects; public class SignInUpAPI extends WebserverAPI { @@ -77,19 +79,30 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I this.getTenantIdentifierWithStorageFromRequest(req), super.main, thirdPartyId, thirdPartyUserId, email, isEmailVerified); + UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{response.user}); - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.id); + ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); result.addProperty("createdNewUser", response.createdNewUser); - JsonObject userJson = new JsonParser().parse(new Gson().toJson(response.user)).getAsJsonObject(); + JsonObject userJson = response.user.toJsonWithoutAccountLinking(); if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { userJson.remove("tenantIds"); } result.add("user", userJson); + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { + for (LoginMethod loginMethod : response.user.loginMethods) { + if (loginMethod.recipeId.equals(RECIPE_ID.THIRD_PARTY) + && Objects.equals(loginMethod.thirdParty.id, thirdPartyId) + && Objects.equals(loginMethod.thirdParty.userId, thirdPartyUserId)) { + result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId()); + break; + } + } + } super.sendJsonResponse(200, result, resp); } catch (StorageQueryException | TenantOrAppNotFoundException e) { @@ -102,6 +115,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject emailObject = InputParser.parseJsonObjectOrThrowError(input, "email", false); String email = InputParser.parseStringOrThrowError(emailObject, "id", false); + // setting email verified behaviour is to be done only for CDI 4.0 onwards. version 3.0 and earlier + // do not have this field + Boolean isEmailVerified = false; + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { + isEmailVerified = InputParser.parseBooleanOrThrowError(emailObject, "isVerified", false); + } + assert thirdPartyId != null; assert thirdPartyUserId != null; assert email != null; @@ -116,32 +136,42 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { ThirdParty.SignInUpResponse response = ThirdParty.signInUp( this.getTenantIdentifierWithStorageFromRequest(req), super.main, thirdPartyId, thirdPartyUserId, - email); - - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.id); + email, isEmailVerified); + UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{response.user}); - // - io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = UserIdMapping - .getUserIdMapping(this.getAppIdentifierWithStorage(req), response.user.id, - UserIdType.SUPERTOKENS); - if (userIdMapping != null) { - response.user.id = userIdMapping.externalUserId; - } + ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); result.addProperty("createdNewUser", response.createdNewUser); - JsonObject userJson = new JsonParser().parse(new Gson().toJson(response.user)).getAsJsonObject(); + JsonObject userJson = + getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? response.user.toJson() : + response.user.toJsonWithoutAccountLinking(); if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { userJson.remove("tenantIds"); } result.add("user", userJson); + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { + for (LoginMethod loginMethod : response.user.loginMethods) { + if (loginMethod.recipeId.equals(RECIPE_ID.THIRD_PARTY) + && Objects.equals(loginMethod.thirdParty.id, thirdPartyId) + && Objects.equals(loginMethod.thirdParty.userId, thirdPartyUserId)) { + result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId()); + break; + } + } + } super.sendJsonResponse(200, result, resp); } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); + } catch (EmailChangeNotAllowedException e) { + JsonObject result = new JsonObject(); + result.addProperty("status", "EMAIL_CHANGE_NOT_ALLOWED_ERROR"); + result.addProperty("reason", "Email already associated with another primary user."); + super.sendJsonResponse(200, result, resp); } } diff --git a/src/main/java/io/supertokens/webserver/api/thirdparty/UserAPI.java b/src/main/java/io/supertokens/webserver/api/thirdparty/UserAPI.java index 9fa23bdfb..2ce566281 100644 --- a/src/main/java/io/supertokens/webserver/api/thirdparty/UserAPI.java +++ b/src/main/java/io/supertokens/webserver/api/thirdparty/UserAPI.java @@ -16,16 +16,14 @@ package io.supertokens.webserver.api.thirdparty; -import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; import io.supertokens.Main; -import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; -import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; -import io.supertokens.pluginInterface.thirdparty.UserInfo; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.thirdparty.ThirdParty; import io.supertokens.useridmapping.UserIdMapping; import io.supertokens.useridmapping.UserIdType; @@ -51,6 +49,7 @@ public String getPath() { return "/recipe/user"; } + @Deprecated @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { // API is tenant specific for get by thirdPartyUserId @@ -72,7 +71,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO } try { - UserInfo user = null; + AuthRecipeUserInfo user = null; if (userId != null) { // Query by userId try { @@ -83,9 +82,10 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO userId = appIdentifierWithStorageAndUserIdMapping.userIdMapping.superTokensUserId; } - user = ThirdParty.getUser(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, userId); - if (user != null && appIdentifierWithStorageAndUserIdMapping.userIdMapping != null) { - user.id = appIdentifierWithStorageAndUserIdMapping.userIdMapping.externalUserId; + user = ThirdParty.getUser(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, + userId); + if (user != null) { + UserIdMapping.populateExternalUserIdForUsers(appIdentifierWithStorageAndUserIdMapping.appIdentifierWithStorage, new AuthRecipeUserInfo[]{user}); } } catch (UnknownUserIdException e) { // let the user be null @@ -94,11 +94,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO user = ThirdParty.getUser(this.getTenantIdentifierWithStorageFromRequest(req), thirdPartyId, thirdPartyUserId); if (user != null) { - io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = UserIdMapping - .getUserIdMapping(this.getAppIdentifierWithStorage(req), user.id, UserIdType.SUPERTOKENS); - if (userIdMapping != null) { - user.id = userIdMapping.externalUserId; - } + UserIdMapping.populateExternalUserIdForUsers(getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{user}); } } @@ -110,7 +106,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO } else { JsonObject result = new JsonObject(); result.addProperty("status", "OK"); - JsonObject userJson = new JsonParser().parse(new Gson().toJson(user)).getAsJsonObject(); + JsonObject userJson = + getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? user.toJson() : + user.toJsonWithoutAccountLinking(); if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { userJson.remove("tenantIds"); diff --git a/src/test/java/io/supertokens/test/AuthRecipeAPITest2_10.java b/src/test/java/io/supertokens/test/AuthRecipeAPITest2_10.java index acb2f0407..e8209e53d 100644 --- a/src/test/java/io/supertokens/test/AuthRecipeAPITest2_10.java +++ b/src/test/java/io/supertokens/test/AuthRecipeAPITest2_10.java @@ -20,7 +20,7 @@ import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.httpRequest.HttpRequestForTesting; import io.supertokens.test.httpRequest.HttpResponseException; @@ -67,11 +67,11 @@ public void deleteUser() throws Exception { assertEquals(response.entrySet().size(), 1); } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test0@example.com", "password0"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test0@example.com", "password0"); { JsonObject requestBody = new JsonObject(); - requestBody.addProperty("userId", user.id); + requestBody.addProperty("userId", user.getSupertokensUserId()); JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/user/remove", requestBody, 1000, 1000, null, @@ -80,7 +80,7 @@ public void deleteUser() throws Exception { assertEquals(response.entrySet().size(), 1); } - assertNull(EmailPassword.getUserUsingId(process.getProcess(), user.id)); + assertNull(EmailPassword.getUserUsingId(process.getProcess(), user.getSupertokensUserId())); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/test/AuthRecipeTest.java b/src/test/java/io/supertokens/test/AuthRecipeTest.java index 42e0ce0db..d941f81cc 100644 --- a/src/test/java/io/supertokens/test/AuthRecipeTest.java +++ b/src/test/java/io/supertokens/test/AuthRecipeTest.java @@ -28,7 +28,6 @@ import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; -import io.supertokens.pluginInterface.emailpassword.UserInfo; import io.supertokens.session.Session; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.thirdparty.ThirdParty; @@ -66,7 +65,7 @@ public void beforeEach() { @Test public void getUsersCount() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -89,17 +88,17 @@ public void getUsersCount() throws Exception { } { - long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }); + long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}); assert (count == 2); } { - long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY }); + long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY}); assert (count == 0); } { - long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.PASSWORDLESS }); + long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.PASSWORDLESS}); assert (count == 0); } @@ -110,28 +109,28 @@ public void getUsersCount() throws Exception { ThirdParty.signInUp(process.getProcess(), thirdPartyId, thirdPartyUserId_1, email_1); { - long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] {}); + long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{}); assert (count == 3); } { long count = AuthRecipe.getUsersCount(process.getProcess(), - new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY }); + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY}); assert (count == 3); } { - long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }); + long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}); assert (count == 2); } { - long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY }); + long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY}); assert (count == 1); } { - long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.PASSWORDLESS }); + long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.PASSWORDLESS}); assert (count == 0); } @@ -147,28 +146,28 @@ public void getUsersCount() throws Exception { } { - long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] {}); + long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{}); assert (count == 5); } { long count = AuthRecipe.getUsersCount(process.getProcess(), - new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.PASSWORDLESS }); + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.PASSWORDLESS}); assert (count == 4); } { - long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }); + long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}); assert (count == 2); } { - long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY }); + long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY}); assert (count == 1); } { - long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { RECIPE_ID.PASSWORDLESS }); + long count = AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{RECIPE_ID.PASSWORDLESS}); assert (count == 2); } process.kill(); @@ -177,7 +176,7 @@ public void getUsersCount() throws Exception { @Test public void paginationTest() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -194,107 +193,107 @@ public void paginationTest() throws Exception { { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null, - new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY }, null); + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 0); } - UserInfo user1 = EmailPassword.signUp(process.getProcess(), "test0@example.com", "password0"); - UserInfo user2 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password1"); - UserInfo user3 = EmailPassword.signUp(process.getProcess(), "test20@example.com", "password2"); - UserInfo user4 = EmailPassword.signUp(process.getProcess(), "test3@example.com", "password3"); + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test0@example.com", "password0"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password1"); + AuthRecipeUserInfo user3 = EmailPassword.signUp(process.getProcess(), "test20@example.com", "password2"); + AuthRecipeUserInfo user4 = EmailPassword.signUp(process.getProcess(), "test3@example.com", "password3"); { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null, - new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }, null); + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 4); - assert (users.users[0].recipeId.equals("emailpassword")); - assert (users.users[0].user.equals(user1)); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user2)); - assert (users.users[2].recipeId.equals("emailpassword")); - assert (users.users[2].user.equals(user3)); - assert (users.users[3].recipeId.equals("emailpassword")); - assert (users.users[3].user.equals(user4)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[0].equals(user1)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user2)); + assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[2].equals(user3)); + assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[3].equals(user4)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null, - new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY }, null); + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 4); - assert (users.users[0].recipeId.equals("emailpassword")); - assert (users.users[0].user.equals(user1)); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user2)); - assert (users.users[2].recipeId.equals("emailpassword")); - assert (users.users[2].user.equals(user3)); - assert (users.users[3].recipeId.equals("emailpassword")); - assert (users.users[3].user.equals(user4)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[0].equals(user1)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user2)); + assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[2].equals(user3)); + assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[3].equals(user4)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null, - new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }, null); + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 4); - assert (users.users[3].recipeId.equals("emailpassword")); - assert (users.users[3].user.equals(user1)); - assert (users.users[2].recipeId.equals("emailpassword")); - assert (users.users[2].user.equals(user2)); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user3)); - assert (users.users[0].recipeId.equals("emailpassword")); - assert (users.users[0].user.equals(user4)); + assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[3].equals(user1)); + assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[2].equals(user2)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user3)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[0].equals(user4)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null, - new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD }, null); + new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 4); - assert (users.users[3].recipeId.equals("emailpassword")); - assert (users.users[3].user.equals(user1)); - assert (users.users[2].recipeId.equals("emailpassword")); - assert (users.users[2].user.equals(user2)); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user3)); - assert (users.users[0].recipeId.equals("emailpassword")); - assert (users.users[0].user.equals(user4)); + assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[3].equals(user1)); + assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[2].equals(user2)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user3)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[0].equals(user4)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null, - new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.SESSION }, null); + new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.SESSION}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 0); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 2, "DESC", null, - new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD }, null); + new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD}, null); assert (users.nextPaginationToken != null); assert (users.users.length == 2); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user3)); - assert (users.users[0].recipeId.equals("emailpassword")); - assert (users.users[0].user.equals(user4)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user3)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[0].equals(user4)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 3, "ASC", null, - new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD }, null); + new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD}, null); assert (users.nextPaginationToken != null); assert (users.users.length == 3); - assert (users.users[0].recipeId.equals("emailpassword")); - assert (users.users[0].user.equals(user1)); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user2)); - assert (users.users[2].recipeId.equals("emailpassword")); - assert (users.users[2].user.equals(user3)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[0].equals(user1)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user2)); + assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[2].equals(user3)); } /////////////////////////////////////////////////////////////////////////////////// @@ -303,124 +302,124 @@ public void paginationTest() throws Exception { String thirdPartyUserId_1 = "thirdPartyUserIdA"; String email_1 = "testA@example.com"; - io.supertokens.pluginInterface.thirdparty.UserInfo user5 = ThirdParty.signInUp(process.getProcess(), + AuthRecipeUserInfo user5 = ThirdParty.signInUp(process.getProcess(), thirdPartyId, thirdPartyUserId_1, email_1).user; { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null, - new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }, null); + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 4); - assert (users.users[0].recipeId.equals("emailpassword")); - assert (users.users[0].user.equals(user1)); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user2)); - assert (users.users[2].recipeId.equals("emailpassword")); - assert (users.users[2].user.equals(user3)); - assert (users.users[3].recipeId.equals("emailpassword")); - assert (users.users[3].user.equals(user4)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[0].equals(user1)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user2)); + assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[2].equals(user3)); + assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[3].equals(user4)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null, - new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY }, null); + new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 1); - assert (users.users[0].recipeId.equals("thirdparty")); - assert (users.users[0].user.equals(user5)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("thirdparty")); + assert (users.users[0].equals(user5)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "ASC", null, - new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY }, null); + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD, RECIPE_ID.THIRD_PARTY}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 5); - assert (users.users[0].recipeId.equals("emailpassword")); - assert (users.users[0].user.equals(user1)); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user2)); - assert (users.users[2].recipeId.equals("emailpassword")); - assert (users.users[2].user.equals(user3)); - assert (users.users[3].recipeId.equals("emailpassword")); - assert (users.users[3].user.equals(user4)); - assert (users.users[4].recipeId.equals("thirdparty")); - assert (users.users[4].user.equals(user5)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[0].equals(user1)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user2)); + assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[2].equals(user3)); + assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[3].equals(user4)); + assert (users.users[4].loginMethods[0].recipeId.toString().equals("thirdparty")); + assert (users.users[4].equals(user5)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null, - new RECIPE_ID[] { RECIPE_ID.EMAIL_PASSWORD }, null); + new RECIPE_ID[]{RECIPE_ID.EMAIL_PASSWORD}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 4); - assert (users.users[3].recipeId.equals("emailpassword")); - assert (users.users[3].user.equals(user1)); - assert (users.users[2].recipeId.equals("emailpassword")); - assert (users.users[2].user.equals(user2)); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user3)); - assert (users.users[0].recipeId.equals("emailpassword")); - assert (users.users[0].user.equals(user4)); + assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[3].equals(user1)); + assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[2].equals(user2)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user3)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[0].equals(user4)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null, - new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY }, null); + new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 1); - assert (users.users[0].recipeId.equals("thirdparty")); - assert (users.users[0].user.equals(user5)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("thirdparty")); + assert (users.users[0].equals(user5)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null, - new RECIPE_ID[] {}, null); + new RECIPE_ID[]{}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 5); - assert (users.users[4].recipeId.equals("emailpassword")); - assert (users.users[4].user.equals(user1)); - assert (users.users[3].recipeId.equals("emailpassword")); - assert (users.users[3].user.equals(user2)); - assert (users.users[2].recipeId.equals("emailpassword")); - assert (users.users[2].user.equals(user3)); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user4)); - assert (users.users[0].recipeId.equals("thirdparty")); - assert (users.users[0].user.equals(user5)); + assert (users.users[4].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[4].equals(user1)); + assert (users.users[3].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[3].equals(user2)); + assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[2].equals(user3)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user4)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("thirdparty")); + assert (users.users[0].equals(user5)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 100, "DESC", null, - new RECIPE_ID[] { RECIPE_ID.SESSION }, null); + new RECIPE_ID[]{RECIPE_ID.SESSION}, null); assert (users.nextPaginationToken == null); assert (users.users.length == 0); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 2, "DESC", null, - new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD }, null); + new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD}, null); assert (users.nextPaginationToken != null); assert (users.users.length == 2); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user4)); - assert (users.users[0].recipeId.equals("thirdparty")); - assert (users.users[0].user.equals(user5)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user4)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("thirdparty")); + assert (users.users[0].equals(user5)); } { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), 3, "ASC", null, - new RECIPE_ID[] { RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD }, null); + new RECIPE_ID[]{RECIPE_ID.THIRD_PARTY, RECIPE_ID.EMAIL_PASSWORD}, null); assert (users.nextPaginationToken != null); assert (users.users.length == 3); - assert (users.users[0].recipeId.equals("emailpassword")); - assert (users.users[0].user.equals(user1)); - assert (users.users[1].recipeId.equals("emailpassword")); - assert (users.users[1].user.equals(user2)); - assert (users.users[2].recipeId.equals("emailpassword")); - assert (users.users[2].user.equals(user3)); + assert (users.users[0].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[0].equals(user1)); + assert (users.users[1].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[1].equals(user2)); + assert (users.users[2].loginMethods[0].recipeId.toString().equals("emailpassword")); + assert (users.users[2].equals(user3)); } process.kill(); @@ -430,8 +429,8 @@ public void paginationTest() throws Exception { @Test public void randomPaginationTest() throws Exception { int numberOfUsers = 500; - int[] limits = new int[] { 10, 14, 20, 23, 50, 100, 110, 150, 200, 510 }; - String[] args = { "../" }; + int[] limits = new int[]{10, 14, 20, 23, 50, 100, 110, 150, 200, 510}; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -440,11 +439,11 @@ public void randomPaginationTest() throws Exception { return; } - Map> signUpMap = getSignUpMap(process); + Map> signUpMap = getSignUpMap(process); - List classes = getUserInfoClassNameList(); + List authRecipes = getAuthRecipes(); - for (String className : classes) { + for (String className : authRecipes) { if (!signUpMap.containsKey(className)) { fail(); } @@ -457,7 +456,7 @@ public void randomPaginationTest() throws Exception { for (int i = 0; i < numberOfUsers; i++) { if (Math.random() > 0.5) { while (true) { - String currUserType = classes.get((int) (Math.random() * classes.size())); + String currUserType = authRecipes.get((int) (Math.random() * authRecipes.size())); AuthRecipeUserInfo user = signUpMap.get(currUserType).apply(null); if (user != null) { synchronized (usersCreated) { @@ -469,7 +468,7 @@ public void randomPaginationTest() throws Exception { } else { es.execute(() -> { while (true) { - String currUserType = classes.get((int) (Math.random() * classes.size())); + String currUserType = authRecipes.get((int) (Math.random() * authRecipes.size())); AuthRecipeUserInfo user = signUpMap.get(currUserType).apply(null); if (user != null) { synchronized (usersCreated) { @@ -497,7 +496,7 @@ public void randomPaginationTest() throws Exception { if (o1.timeJoined != o2.timeJoined) { return (int) (o1.timeJoined - o2.timeJoined); } - return o2.id.compareTo(o1.id); + return o2.getSupertokensUserId().compareTo(o1.getSupertokensUserId()); }); // we make sure it's sorted properly.. @@ -516,11 +515,11 @@ public void randomPaginationTest() throws Exception { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), limit, "ASC", paginationToken, null, null); - for (UserPaginationContainer.UsersContainer uc : users.users) { + for (AuthRecipeUserInfo uc : users.users) { AuthRecipeUserInfo expected = usersCreated.get(indexIntoUsers); - AuthRecipeUserInfo actualUser = uc.user; + AuthRecipeUserInfo actualUser = uc; - assert (actualUser.equals(expected) && uc.recipeId.equals(expected.getRecipeId().toString())); + assert (actualUser.equals(expected) && uc.loginMethods[0].recipeId.toString().equals(expected.loginMethods[0].recipeId.toString())); indexIntoUsers++; } @@ -537,7 +536,7 @@ public void randomPaginationTest() throws Exception { if (o1.timeJoined != o2.timeJoined) { return (int) (o1.timeJoined - o2.timeJoined); } - return o1.id.compareTo(o2.id); + return o1.getSupertokensUserId().compareTo(o2.getSupertokensUserId()); }); // we make sure it's sorted properly.. @@ -555,11 +554,11 @@ public void randomPaginationTest() throws Exception { UserPaginationContainer users = AuthRecipe.getUsers(process.getProcess(), limit, "DESC", paginationToken, null, null); - for (UserPaginationContainer.UsersContainer uc : users.users) { + for (AuthRecipeUserInfo uc : users.users) { AuthRecipeUserInfo expected = usersCreated.get(indexIntoUsers); - AuthRecipeUserInfo actualUser = uc.user; + AuthRecipeUserInfo actualUser = uc; - assert (actualUser.equals(expected) && uc.recipeId.equals(expected.getRecipeId().toString())); + assert (actualUser.equals(expected) && uc.loginMethods[0].recipeId.toString().equals(expected.loginMethods[0].recipeId.toString())); indexIntoUsers--; } @@ -576,7 +575,7 @@ public void randomPaginationTest() throws Exception { @Test public void deleteUserTest() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -585,43 +584,43 @@ public void deleteUserTest() throws Exception { return; } - Map> signUpMap = getSignUpMap(process); + Map> signUpMap = getSignUpMap(process); - List classes = getUserInfoClassNameList(); + List authRecipes = getAuthRecipes(); - for (String className : classes) { + for (String className : authRecipes) { if (!signUpMap.containsKey(className)) { fail(); } } - for (String userType : classes) { + for (String userType : authRecipes) { AuthRecipeUserInfo user1 = signUpMap.get(userType).apply(null); JsonObject testMetadata = new JsonObject(); testMetadata.addProperty("test", "test"); - UserMetadata.updateUserMetadata(process.getProcess(), user1.id, testMetadata); - Session.createNewSession(process.getProcess(), user1.id, new JsonObject(), new JsonObject()); + UserMetadata.updateUserMetadata(process.getProcess(), user1.getSupertokensUserId(), testMetadata); + Session.createNewSession(process.getProcess(), user1.getSupertokensUserId(), new JsonObject(), new JsonObject()); String emailVerificationToken = EmailVerification.generateEmailVerificationToken(process.getProcess(), - user1.id, "email"); + user1.getSupertokensUserId(), "email"); EmailVerification.verifyEmail(process.getProcess(), emailVerificationToken); AuthRecipeUserInfo user2 = signUpMap.get(userType).apply(null); - Session.createNewSession(process.getProcess(), user2.id, new JsonObject(), new JsonObject()); + Session.createNewSession(process.getProcess(), user2.getSupertokensUserId(), new JsonObject(), new JsonObject()); String emailVerificationToken2 = EmailVerification.generateEmailVerificationToken(process.getProcess(), - user2.id, "email"); + user2.getSupertokensUserId(), "email"); - assertEquals(2, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { user1.getRecipeId() })); - AuthRecipe.deleteUser(process.getProcess(), user1.id); - assertEquals(1, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { user1.getRecipeId() })); - assertEquals(0, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user1.id).length); - assertEquals(1, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user2.id).length); - assertFalse(EmailVerification.isEmailVerified(process.getProcess(), user1.id, "email")); - assertEquals(0, UserMetadata.getUserMetadata(process.getProcess(), user1.id).entrySet().size()); + assertEquals(2, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{user1.loginMethods[0].recipeId})); + AuthRecipe.deleteUser(process.getProcess(), user1.getSupertokensUserId()); + assertEquals(1, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{user1.loginMethods[0].recipeId})); + assertEquals(0, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user1.getSupertokensUserId()).length); + assertEquals(1, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user2.getSupertokensUserId()).length); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), user1.getSupertokensUserId(), "email")); + assertEquals(0, UserMetadata.getUserMetadata(process.getProcess(), user1.getSupertokensUserId()).entrySet().size()); - AuthRecipe.deleteUser(process.getProcess(), user2.id); - assertEquals(0, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[] { user1.getRecipeId() })); - assertEquals(0, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user2.id).length); - assertEquals(0, UserMetadata.getUserMetadata(process.getProcess(), user2.id).entrySet().size()); + AuthRecipe.deleteUser(process.getProcess(), user2.getSupertokensUserId()); + assertEquals(0, AuthRecipe.getUsersCount(process.getProcess(), new RECIPE_ID[]{user1.loginMethods[0].recipeId})); + assertEquals(0, Session.getAllNonExpiredSessionHandlesForUser(process.getProcess(), user2.getSupertokensUserId()).length); + assertEquals(0, UserMetadata.getUserMetadata(process.getProcess(), user2.getSupertokensUserId()).entrySet().size()); Exception error = null; try { @@ -636,19 +635,16 @@ public void deleteUserTest() throws Exception { assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } - private static List getUserInfoClassNameList() { - Reflections reflections = new Reflections("io.supertokens"); - Set> classes = reflections.getSubTypesOf(AuthRecipeUserInfo.class); - - return classes.stream().map(Class::getCanonicalName).collect(Collectors.toList()); + private static List getAuthRecipes() { + return Arrays.asList("emailpassword", "thirdparty", "passwordless"); } - private static Map> getSignUpMap( + private static Map> getSignUpMap( TestingProcessManager.TestingProcess process) { AtomicInteger count = new AtomicInteger(); - Map> signUpMap = new HashMap<>(); - signUpMap.put("io.supertokens.pluginInterface.emailpassword.UserInfo", o -> { + Map> signUpMap = new HashMap<>(); + signUpMap.put("emailpassword", o -> { try { return EmailPassword.signUp(process.getProcess(), "test" + count.getAndIncrement() + "@example.com", "password0"); @@ -656,7 +652,7 @@ private static List getUserInfoClassNameList() { } return null; }); - signUpMap.put("io.supertokens.pluginInterface.thirdparty.UserInfo", o -> { + signUpMap.put("thirdparty", o -> { try { String thirdPartyId = "testThirdParty"; String thirdPartyUserId = "thirdPartyUserId" + count.getAndIncrement(); @@ -667,7 +663,7 @@ private static List getUserInfoClassNameList() { } return null; }); - signUpMap.put("io.supertokens.pluginInterface.passwordless.UserInfo", o -> { + signUpMap.put("passwordless", o -> { try { String email = "test" + count.getAndIncrement() + "@example.com"; CreateCodeResponse createCode = Passwordless.createCode(process.getProcess(), email, null, null, null); diff --git a/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java b/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java new file mode 100644 index 000000000..3e65fc432 --- /dev/null +++ b/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test; + +import io.supertokens.ProcessState; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.emailpassword.exceptions.WrongCredentialsException; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.thirdparty.ThirdParty; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class AuthRecipesParallelTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void timeTakenFor500SignInParallel() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + ExecutorService ex = Executors.newFixedThreadPool(1000); + int numberOfThreads = 500; + + EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + AtomicInteger counter = new AtomicInteger(0); + AtomicInteger retryCounter = new AtomicInteger(0); + + long st = System.currentTimeMillis(); + for (int i = 0; i < numberOfThreads; i++) { + ex.execute(() -> { + while(true) { + try { + EmailPassword.signIn(process.getProcess(), "test@example.com", "password"); + counter.incrementAndGet(); + break; + } catch (StorageQueryException e) { + retryCounter.incrementAndGet(); + // continue + } catch (WrongCredentialsException e) { + throw new RuntimeException(e); + } + } + }); + } + + ex.shutdown(); + + ex.awaitTermination(2, TimeUnit.MINUTES); + System.out.println("Time taken for " + numberOfThreads + " sign in parallel: " + (System.currentTimeMillis() - st) + "ms"); + System.out.println("Retry counter: " + retryCounter.get()); + assertEquals(counter.get(), numberOfThreads); + assertEquals(0, retryCounter.get()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void timeTakenFor500SignInUpParallel() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + ExecutorService ex = Executors.newFixedThreadPool(1000); + int numberOfThreads = 500; + + ThirdParty.signInUp(process.getProcess(), "google", "google-user", "test@example.com"); + AtomicInteger counter = new AtomicInteger(0); + AtomicInteger retryCounter = new AtomicInteger(0); + + ThirdParty.signInUp(process.getProcess(), "google", "google-user", "test@example.com"); + + long st = System.currentTimeMillis(); + for (int i = 0; i < numberOfThreads; i++) { + ex.execute(() -> { + while(true) { + try { + ThirdParty.signInUp(process.getProcess(), "google", "google-user", "test@example.com"); + counter.incrementAndGet(); + break; + } catch (StorageQueryException e) { + retryCounter.incrementAndGet(); + // continue + } catch (EmailChangeNotAllowedException e) { + throw new RuntimeException(e); + } + } + }); + } + + ex.shutdown(); + + ex.awaitTermination(2, TimeUnit.MINUTES); + System.out.println("Time taken for " + numberOfThreads + " sign in parallel: " + (System.currentTimeMillis() - st) + "ms"); + System.out.println("Retry counter: " + retryCounter.get()); + assertEquals (counter.get(), numberOfThreads); + assertEquals(0, retryCounter.get()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/FeatureFlagTest.java b/src/test/java/io/supertokens/test/FeatureFlagTest.java index 15be7c36e..c127f666a 100644 --- a/src/test/java/io/supertokens/test/FeatureFlagTest.java +++ b/src/test/java/io/supertokens/test/FeatureFlagTest.java @@ -32,7 +32,9 @@ import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.featureflag.exceptions.NoLicenseKeyFoundException; import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.MultitenancyHelper; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; @@ -509,6 +511,7 @@ public void testThatMultitenantStatsAreAccurateForAnApp() throws Exception { null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion().get(), ""); Assert.assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("usageStats").getAsJsonObject().has("account_linking")); JsonArray multitenancyStats = response.get("usageStats").getAsJsonObject().get("multi_tenancy") .getAsJsonObject().get("tenants").getAsJsonArray(); assertEquals(6, multitenancyStats.size()); diff --git a/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java index 4da18541d..1e565cec5 100644 --- a/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java +++ b/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java @@ -186,6 +186,10 @@ public void testCreatingSessionWithAndWithoutAPIKey() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + String userId = "userId"; JsonObject userDataInJWT = new JsonObject(); userDataInJWT.addProperty("key", "value"); @@ -209,7 +213,7 @@ public void testCreatingSessionWithAndWithoutAPIKey() throws Exception { JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", request, 1000, 1000, null, - Utils.getCdiVersionStringLatestForTests(), + SemVer.v3_0.get(), apiKey, ""); assertEquals(sessionInfo.get("status").getAsString(), "OK"); checkSessionResponse(sessionInfo, process, userId, userDataInJWT); @@ -272,6 +276,10 @@ public void testCreatingSessionWithAndWithoutAPIKeyWhenSuperTokensSaaSSecretIsAl TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + String userId = "userId"; JsonObject userDataInJWT = new JsonObject(); userDataInJWT.addProperty("key", "value"); @@ -296,7 +304,7 @@ public void testCreatingSessionWithAndWithoutAPIKeyWhenSuperTokensSaaSSecretIsAl { JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", request, 1000, 1000, null, - Utils.getCdiVersionStringLatestForTests(), + SemVer.v3_0.get(), apiKey, ""); assertEquals(sessionInfo.get("status").getAsString(), "OK"); checkSessionResponse(sessionInfo, process, userId, userDataInJWT); @@ -305,7 +313,7 @@ public void testCreatingSessionWithAndWithoutAPIKeyWhenSuperTokensSaaSSecretIsAl { JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session", request, 1000, 1000, null, - Utils.getCdiVersionStringLatestForTests(), + SemVer.v3_0.get(), saasSecret, ""); assertEquals(sessionInfo.get("status").getAsString(), "OK"); checkSessionResponse(sessionInfo, process, userId, userDataInJWT); diff --git a/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java b/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java new file mode 100644 index 000000000..5614de91d --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java @@ -0,0 +1,549 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; +import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithPrimaryUserIdException; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.thirdparty.ThirdParty; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.assertNotNull; + + +public class CreatePrimaryUserTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testThatOnSignUpUserIsNotAPrimaryUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + assert (!user.isPrimaryUser); + assert (user.loginMethods.length == 1); + assert (user.loginMethods[0].recipeId == RECIPE_ID.EMAIL_PASSWORD); + assert (user.loginMethods[0].email.equals("test@example.com")); + assert (user.loginMethods[0].passwordHash != null); + assert (user.loginMethods[0].thirdParty == null); + assert (user.getSupertokensUserId().equals(user.loginMethods[0].getSupertokensUserId())); + assert (user.loginMethods[0].phoneNumber == null); + + ThirdParty.SignInUpResponse resp = ThirdParty.signInUp(process.getProcess(), "google", "user-google", + "test@example.com"); + assert (!resp.user.isPrimaryUser); + assert (resp.user.loginMethods.length == 1); + assert (resp.user.loginMethods[0].recipeId == RECIPE_ID.THIRD_PARTY); + assert (resp.user.loginMethods[0].email.equals("test@example.com")); + assert (resp.user.loginMethods[0].thirdParty.userId.equals("user-google")); + assert (resp.user.loginMethods[0].thirdParty.id.equals("google")); + assert (resp.user.loginMethods[0].phoneNumber == null); + assert (resp.user.loginMethods[0].passwordHash == null); + assert (resp.user.getSupertokensUserId().equals(resp.user.loginMethods[0].getSupertokensUserId())); + + { + Passwordless.CreateCodeResponse code = Passwordless.createCode(process.getProcess(), "u@e.com", null, null, + null); + Passwordless.ConsumeCodeResponse pResp = Passwordless.consumeCode(process.getProcess(), code.deviceId, + code.deviceIdHash, code.userInputCode, null); + assert (!pResp.user.isPrimaryUser); + assert (pResp.user.loginMethods.length == 1); + assert (pResp.user.loginMethods[0].recipeId == RECIPE_ID.PASSWORDLESS); + assert (pResp.user.loginMethods[0].email.equals("u@e.com")); + assert (pResp.user.loginMethods[0].passwordHash == null); + assert (pResp.user.loginMethods[0].thirdParty == null); + assert (pResp.user.loginMethods[0].phoneNumber == null); + assert (pResp.user.getSupertokensUserId().equals(pResp.user.loginMethods[0].getSupertokensUserId())); + } + + { + Passwordless.CreateCodeResponse code = Passwordless.createCode(process.getProcess(), null, "12345", null, + null); + Passwordless.ConsumeCodeResponse pResp = Passwordless.consumeCode(process.getProcess(), code.deviceId, + code.deviceIdHash, code.userInputCode, null); + assert (!pResp.user.isPrimaryUser); + assert (pResp.user.loginMethods.length == 1); + assert (pResp.user.loginMethods[0].recipeId == RECIPE_ID.PASSWORDLESS); + assert (pResp.user.loginMethods[0].email == null); + assert (pResp.user.loginMethods[0].passwordHash == null); + assert (pResp.user.loginMethods[0].thirdParty == null); + assert (pResp.user.loginMethods[0].phoneNumber.equals("12345")); + assert (pResp.user.getSupertokensUserId().equals(pResp.user.loginMethods[0].getSupertokensUserId())); + } + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatCreationOfPrimaryUserRequiresAccountLinkingFeatureToBeEnabled() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.isInMemDb(process.getProcess())) { + // Features are enabled by default for inmemory db + return; + } + + try { + AuthRecipe.createPrimaryUser(process.main, + new AppIdentifierWithStorage(null, null, StorageLayer.getStorage(process.main)), ""); + assert (false); + } catch (FeatureNotEnabledException e) { + assert (e.getMessage() + .equals("Account linking feature is not enabled for this app. Please contact support to enable it" + + ".")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makeEmailPasswordPrimaryUserSuccess() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + assert (result.user.isPrimaryUser); + assert (result.user.loginMethods.length == 1); + assert (result.user.loginMethods[0].recipeId == RECIPE_ID.EMAIL_PASSWORD); + assert (result.user.loginMethods[0].email.equals("test@example.com")); + assert (result.user.loginMethods[0].passwordHash != null); + assert (result.user.loginMethods[0].thirdParty == null); + assert (result.user.getSupertokensUserId().equals(result.user.loginMethods[0].getSupertokensUserId())); + assert (result.user.loginMethods[0].phoneNumber == null); + + AuthRecipeUserInfo refetchedUser = AuthRecipe.getUserById(process.main, result.user.getSupertokensUserId()); + + assert (refetchedUser.equals(result.user)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makeThirdPartyPrimaryUserSuccess() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + ThirdParty.SignInUpResponse signInUp = ThirdParty.signInUp(process.getProcess(), "google", + "user-google", + "test@example.com"); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, + signInUp.user.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + assert (result.user.isPrimaryUser); + assert (result.user.loginMethods.length == 1); + assert (result.user.loginMethods[0].recipeId == RECIPE_ID.THIRD_PARTY); + assert (result.user.loginMethods[0].email.equals("test@example.com")); + assert (result.user.loginMethods[0].thirdParty.userId.equals("user-google")); + assert (result.user.loginMethods[0].thirdParty.id.equals("google")); + assert (result.user.loginMethods[0].phoneNumber == null); + assert (result.user.loginMethods[0].passwordHash == null); + assert (result.user.getSupertokensUserId().equals(result.user.loginMethods[0].getSupertokensUserId())); + + AuthRecipeUserInfo refetchedUser = AuthRecipe.getUserById(process.main, result.user.getSupertokensUserId()); + + assert (refetchedUser.equals(result.user)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makePasswordlessEmailPrimaryUserSuccess() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + Passwordless.CreateCodeResponse code = Passwordless.createCode(process.getProcess(), "u@e.com", null, null, + null); + Passwordless.ConsumeCodeResponse pResp = Passwordless.consumeCode(process.getProcess(), code.deviceId, + code.deviceIdHash, code.userInputCode, null); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, + pResp.user.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + assert (result.user.isPrimaryUser); + assert (result.user.loginMethods.length == 1); + assert (result.user.loginMethods[0].recipeId == RECIPE_ID.PASSWORDLESS); + assert (result.user.loginMethods[0].email.equals("u@e.com")); + assert (result.user.loginMethods[0].passwordHash == null); + assert (result.user.loginMethods[0].thirdParty == null); + assert (result.user.loginMethods[0].phoneNumber == null); + assert (result.user.getSupertokensUserId().equals(result.user.loginMethods[0].getSupertokensUserId())); + + AuthRecipeUserInfo refetchedUser = AuthRecipe.getUserById(process.main, result.user.getSupertokensUserId()); + + assert (refetchedUser.equals(result.user)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makePasswordlessPhonePrimaryUserSuccess() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + Passwordless.CreateCodeResponse code = Passwordless.createCode(process.getProcess(), null, "1234", null, + null); + Passwordless.ConsumeCodeResponse pResp = Passwordless.consumeCode(process.getProcess(), code.deviceId, + code.deviceIdHash, code.userInputCode, null); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, + pResp.user.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + assert (result.user.isPrimaryUser); + assert (result.user.loginMethods.length == 1); + assert (result.user.loginMethods[0].recipeId == RECIPE_ID.PASSWORDLESS); + assert (result.user.loginMethods[0].email == null); + assert (result.user.loginMethods[0].passwordHash == null); + assert (result.user.loginMethods[0].thirdParty == null); + assert (result.user.loginMethods[0].phoneNumber.equals("1234")); + assert (result.user.getSupertokensUserId().equals(result.user.loginMethods[0].getSupertokensUserId())); + + AuthRecipeUserInfo refetchedUser = AuthRecipe.getUserById(process.main, result.user.getSupertokensUserId()); + + assert (refetchedUser.equals(result.user)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void alreadyPrimaryUsertest() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (result.wasAlreadyAPrimaryUser); + assert (result.user.getSupertokensUserId().equals(emailPasswordUser.getSupertokensUserId())); + assert (result.user.isPrimaryUser); + assert (result.user.loginMethods.length == 1); + assert (result.user.loginMethods[0].recipeId == RECIPE_ID.EMAIL_PASSWORD); + assert (result.user.loginMethods[0].email.equals("test@example.com")); + assert (result.user.loginMethods[0].passwordHash != null); + assert (result.user.loginMethods[0].thirdParty == null); + assert (result.user.getSupertokensUserId().equals(result.user.loginMethods[0].getSupertokensUserId())); + assert (result.user.loginMethods[0].phoneNumber == null); + + AuthRecipeUserInfo refetchedUser = AuthRecipe.getUserById(process.main, result.user.getSupertokensUserId()); + + assert (refetchedUser.equals(result.user)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makePrimaryUserFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test@example.com"); + + try { + AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId()); + assert (false); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + assert (e.primaryUserId.equals(emailPasswordUser.getSupertokensUserId())); + assert (e.getMessage().equals("This user's email is already associated with another user ID")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makePrimarySucceedsEvenIfAnotherAccountWithSameEmailButIsNotAPrimaryUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test@example.com"); + + AuthRecipe.CreatePrimaryUserResult r = AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId()); + assert (!r.wasAlreadyAPrimaryUser); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makePrimaryUserFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUserInAnotherTenant() + throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), + new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true), + new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(true), + new JsonObject())); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", + StorageLayer.getStorage(process.main)); + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(tenantIdentifierWithStorage, process.getProcess(), + "test@example.com", + "pass1234"); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test@example.com"); + + Multitenancy.addUserIdToTenant(process.main, tenantIdentifierWithStorage, signInUpResponse.user.getSupertokensUserId()); + + try { + AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId()); + assert (false); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + assert (e.primaryUserId.equals(emailPasswordUser.getSupertokensUserId())); + assert (e.getMessage().equals("This user's email is already associated with another user ID")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makePrimarySucceedsEvenIfAnotherAccountWithSameEmailButInADifferentTenant() + throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), + new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true), + new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(true), + new JsonObject())); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", + StorageLayer.getStorage(process.main)); + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(tenantIdentifierWithStorage, process.getProcess(), + "test@example.com", + "pass1234"); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test@example.com"); + + AuthRecipe.CreatePrimaryUserResult r = AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId()); + assert !r.wasAlreadyAPrimaryUser; + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makePrimaryUserFailsCauseOfUnknownUserId() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + try { + AuthRecipe.createPrimaryUser(process.main, "random"); + assert (false); + } catch (UnknownUserIdException ignored) { + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makingPrimaryUserFailsCauseAlreadyLinkedToAnotherAccount() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser1 = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + AuthRecipeUserInfo emailPasswordUser2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, emailPasswordUser2.getSupertokensUserId(), emailPasswordUser1.getSupertokensUserId()); + + try { + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser2.getSupertokensUserId()); + assert (false); + } catch (RecipeUserIdAlreadyLinkedWithPrimaryUserIdException e) { + assert (e.primaryUserId.equals(emailPasswordUser1.getSupertokensUserId())); + assert (e.getMessage().equals("This user ID is already linked to another user ID")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/DeleteUserTest.java b/src/test/java/io/supertokens/test/accountlinking/DeleteUserTest.java new file mode 100644 index 000000000..edc07511d --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/DeleteUserTest.java @@ -0,0 +1,394 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.useridmapping.UserIdType; +import io.supertokens.usermetadata.UserMetadata; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + + +public class DeleteUserTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void deleteLinkedUserWithoutRemovingAllUsers() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123"); + + AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123"); + + AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId()); + + assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked); + + AuthRecipe.deleteUser(process.main, r1.getSupertokensUserId(), false); + + assertNull(AuthRecipe.getUserById(process.main, r1.getSupertokensUserId())); + + AuthRecipeUserInfo user = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId()); + + assert (user.loginMethods.length == 1); + assert (user.isPrimaryUser); + assert (user.getSupertokensUserId().equals(r2.getSupertokensUserId())); + assert (user.loginMethods[0].getSupertokensUserId().equals(r2.getSupertokensUserId())); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void deleteLinkedPrimaryUserWithoutRemovingAllUsers() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123"); + + AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123"); + + AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId()); + + assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked); + + AuthRecipe.deleteUser(process.main, r2.getSupertokensUserId(), false); + + AuthRecipeUserInfo userP = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId()); + + AuthRecipeUserInfo user = AuthRecipe.getUserById(process.main, r1.getSupertokensUserId()); + + assert (user.loginMethods.length == 1); + assert (user.isPrimaryUser); + assert (user.getSupertokensUserId().equals(r2.getSupertokensUserId())); + assert (user.loginMethods[0].getSupertokensUserId().equals(r1.getSupertokensUserId())); + assert (userP.equals(user)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void deleteLinkedPrimaryUserRemovingAllUsers() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123"); + + AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123"); + + AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId()); + + assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked); + + AuthRecipe.deleteUser(process.main, r2.getSupertokensUserId()); + + AuthRecipeUserInfo userP = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId()); + + AuthRecipeUserInfo user = AuthRecipe.getUserById(process.main, r1.getSupertokensUserId()); + + assert (user == null && userP == null); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void deleteLinkedPrimaryUserRemovingAllUsers2() throws Exception { + + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123"); + + AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123"); + + AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId()); + + assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked); + + AuthRecipe.deleteUser(process.main, r1.getSupertokensUserId()); + + AuthRecipeUserInfo userP = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId()); + + AuthRecipeUserInfo user = AuthRecipe.getUserById(process.main, r1.getSupertokensUserId()); + + assert (user == null && userP == null); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void deleteUserTestWithUserIdMapping1() throws Exception { + /* + * recipe user r1 is mapped to e1 which has some metadata with e1 as the key. r1 gets linked to r2 which is + * mapped to e2 with some metadata associated with it. Now we want to delete r1. This should clear r1 entry, + * e1 entry, and e1 metadata, but should not clear e2 stuff at all. + * */ + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123"); + UserIdMapping.createUserIdMapping(process.main, r1.getSupertokensUserId(), "e1", null, false); + JsonObject metadata = new JsonObject(); + metadata.addProperty("k1", "v1"); + UserMetadata.updateUserMetadata(process.main, "e1", metadata); + + AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123"); + UserIdMapping.createUserIdMapping(process.main, r2.getSupertokensUserId(), "e2", null, false); + UserMetadata.updateUserMetadata(process.main, "e2", metadata); + + AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId()); + + assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked); + + AuthRecipe.deleteUser(process.main, r1.getSupertokensUserId(), false); + + assertNull(AuthRecipe.getUserById(process.main, r1.getSupertokensUserId())); + + assertNull(AuthRecipe.getUserById(process.main, "e2")); + + assertNotNull(AuthRecipe.getUserById(process.main, r2.getSupertokensUserId())); + + assertEquals(UserMetadata.getUserMetadata(process.main, "e1"), new JsonObject()); + assertEquals(UserMetadata.getUserMetadata(process.main, r1.getSupertokensUserId()), new JsonObject()); + assertEquals(UserMetadata.getUserMetadata(process.main, "e2"), metadata); + assertEquals(UserMetadata.getUserMetadata(process.main, r2.getSupertokensUserId()), new JsonObject()); + assert (UserIdMapping.getUserIdMapping(process.main, r2.getSupertokensUserId(), UserIdType.SUPERTOKENS) != null); + assert (UserIdMapping.getUserIdMapping(process.main, r1.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null); + + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void deleteUserTestWithUserIdMapping2() throws Exception { + /* + * recipe user r1 exists. r1 gets linked to r2 which is mapped to e2 with some metadata associated with it. + * Now we want to delete r1 with linked all recipes as true. This should clear r1, r2 entry clear e2 metadata. + * */ + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123"); + UserIdMapping.createUserIdMapping(process.main, r1.getSupertokensUserId(), "e1", null, false); + JsonObject metadata = new JsonObject(); + metadata.addProperty("k1", "v1"); + UserMetadata.updateUserMetadata(process.main, "e1", metadata); + + AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123"); + UserIdMapping.createUserIdMapping(process.main, r2.getSupertokensUserId(), "e2", null, false); + UserMetadata.updateUserMetadata(process.main, "e2", metadata); + + AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId()); + + assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked); + + AuthRecipe.deleteUser(process.main, r1.getSupertokensUserId()); + + assertNull(AuthRecipe.getUserById(process.main, r1.getSupertokensUserId())); + + assertNull(AuthRecipe.getUserById(process.main, "e2")); + + assertNull(AuthRecipe.getUserById(process.main, r2.getSupertokensUserId())); + + assertEquals(UserMetadata.getUserMetadata(process.main, "e1"), new JsonObject()); + assertEquals(UserMetadata.getUserMetadata(process.main, r1.getSupertokensUserId()), new JsonObject()); + assertEquals(UserMetadata.getUserMetadata(process.main, "e2"), new JsonObject()); + assertEquals(UserMetadata.getUserMetadata(process.main, r2.getSupertokensUserId()), new JsonObject()); + assert (UserIdMapping.getUserIdMapping(process.main, r2.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null); + assert (UserIdMapping.getUserIdMapping(process.main, r1.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null); + + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void deleteUserTestWithUserIdMapping3() throws Exception { + /* + * three recipes are linked, r1, r2, r3 (primary user is r1). We have external user ID mapping for all three + * with some metadata. First we delete r1. This should not delete metadata and linked accounts. Then we + * delete r2 - this should delete metadata of r2 and r2. The we delete r3 - this should delete metadata of r3 + * and r1 + * */ + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo r1 = EmailPassword.signUp(process.main, "test@example.com", "pass123"); + UserIdMapping.createUserIdMapping(process.main, r1.getSupertokensUserId(), "e1", null, false); + JsonObject metadata = new JsonObject(); + metadata.addProperty("k1", "v1"); + UserMetadata.updateUserMetadata(process.main, "e1", metadata); + + AuthRecipeUserInfo r2 = EmailPassword.signUp(process.main, "test2@example.com", "pass123"); + UserIdMapping.createUserIdMapping(process.main, r2.getSupertokensUserId(), "e2", null, false); + UserMetadata.updateUserMetadata(process.main, "e2", metadata); + + AuthRecipeUserInfo r3 = EmailPassword.signUp(process.main, "test3@example.com", "pass123"); + UserIdMapping.createUserIdMapping(process.main, r3.getSupertokensUserId(), "e3", null, false); + UserMetadata.updateUserMetadata(process.main, "e3", metadata); + + AuthRecipe.createPrimaryUser(process.main, r2.getSupertokensUserId()); + + assert (!AuthRecipe.linkAccounts(process.main, r1.getSupertokensUserId(), r2.getSupertokensUserId()).wasAlreadyLinked); + assert (!AuthRecipe.linkAccounts(process.main, r3.getSupertokensUserId(), r1.getSupertokensUserId()).wasAlreadyLinked); + + AuthRecipe.deleteUser(process.main, r1.getSupertokensUserId(), false); + + assertNull(AuthRecipe.getUserById(process.main, r1.getSupertokensUserId())); + + assertEquals(UserMetadata.getUserMetadata(process.main, "e1"), new JsonObject()); + assertEquals(UserMetadata.getUserMetadata(process.main, r1.getSupertokensUserId()), new JsonObject()); + + { + AuthRecipeUserInfo userR2 = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId()); + AuthRecipeUserInfo userR3 = AuthRecipe.getUserById(process.main, r3.getSupertokensUserId()); + assert (userR2.equals(userR3)); + assert (userR2.loginMethods.length == 2); + assertEquals(UserMetadata.getUserMetadata(process.main, "e2"), metadata); + assertEquals(UserMetadata.getUserMetadata(process.main, "e3"), metadata); + assert (UserIdMapping.getUserIdMapping(process.main, r2.getSupertokensUserId(), UserIdType.SUPERTOKENS) != null); + assert (UserIdMapping.getUserIdMapping(process.main, r3.getSupertokensUserId(), UserIdType.SUPERTOKENS) != null); + assert (UserIdMapping.getUserIdMapping(process.main, r1.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null); + } + + AuthRecipe.deleteUser(process.main, r2.getSupertokensUserId(), false); + + { + AuthRecipeUserInfo userR2 = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId()); + AuthRecipeUserInfo userR3 = AuthRecipe.getUserById(process.main, r3.getSupertokensUserId()); + assert (userR2.equals(userR3)); + assert (userR2.loginMethods.length == 1); + assertEquals(UserMetadata.getUserMetadata(process.main, "e2"), metadata); + assertEquals(UserMetadata.getUserMetadata(process.main, "e3"), metadata); + assert (UserIdMapping.getUserIdMapping(process.main, r2.getSupertokensUserId(), UserIdType.SUPERTOKENS) != null); + assert (UserIdMapping.getUserIdMapping(process.main, r3.getSupertokensUserId(), UserIdType.SUPERTOKENS) != null); + assert (UserIdMapping.getUserIdMapping(process.main, r1.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null); + } + + AuthRecipe.deleteUser(process.main, r3.getSupertokensUserId(), false); + + { + AuthRecipeUserInfo userR2 = AuthRecipe.getUserById(process.main, r2.getSupertokensUserId()); + AuthRecipeUserInfo userR3 = AuthRecipe.getUserById(process.main, r3.getSupertokensUserId()); + assert (userR2 == null && userR3 == null); + assertEquals(UserMetadata.getUserMetadata(process.main, "e2"), new JsonObject()); + assertEquals(UserMetadata.getUserMetadata(process.main, "e3"), new JsonObject()); + assert (UserIdMapping.getUserIdMapping(process.main, r2.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null); + assert (UserIdMapping.getUserIdMapping(process.main, r3.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null); + assert (UserIdMapping.getUserIdMapping(process.main, r1.getSupertokensUserId(), UserIdType.SUPERTOKENS) == null); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + +} diff --git a/src/test/java/io/supertokens/test/accountlinking/GetUserByAccountInfoTest.java b/src/test/java/io/supertokens/test/accountlinking/GetUserByAccountInfoTest.java new file mode 100644 index 000000000..11371bf97 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/GetUserByAccountInfoTest.java @@ -0,0 +1,411 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking; + +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.*; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; +import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.thirdparty.ThirdParty; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import static org.junit.Assert.*; + +public class GetUserByAccountInfoTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password) + throws DuplicateEmailException, StorageQueryException { + return EmailPassword.signUp(main, email, password); + } + + AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email) + throws EmailChangeNotAllowedException, StorageQueryException { + return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithPhone(Main main, String phone) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, null, phone, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + @Test + public void testListUsersByAccountInfoForUnlinkedAccounts() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test2@example.com"); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test3@example.com"); + AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(StorageLayer.getBaseStorage(process.getProcess())); + + AuthRecipeUserInfo userToTest = AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test1@example.com", null, null, null)[0]; + assertNotNull(userToTest.getSupertokensUserId()); + assertFalse(userToTest.isPrimaryUser); + assertEquals(1, userToTest.loginMethods.length); + assertEquals("test1@example.com", userToTest.loginMethods[0].email); + assertEquals(RECIPE_ID.EMAIL_PASSWORD, userToTest.loginMethods[0].recipeId); + assertEquals(user1.getSupertokensUserId(), userToTest.loginMethods[0].getSupertokensUserId()); + assertFalse(userToTest.loginMethods[0].verified); + assert(userToTest.loginMethods[0].timeJoined > 0); + + // test for result + assertEquals(user1, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test1@example.com", null, null, null)[0]); + assertEquals(user2, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, null, null, "google", "userid1")[0]); + assertEquals(user2, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test2@example.com", null, "google", "userid1")[0]); + assertEquals(user3, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test3@example.com", null, null, null)[0]); + assertEquals(user4, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, null, "+919876543210", null, null)[0]); + + // test for no result + assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test1@example.com", "+919876543210", null, null).length); + assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test2@example.com", "+919876543210", null, null).length); + assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test3@example.com", "+919876543210", null, null).length); + assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, null, "+919876543210", "google", "userid1").length); + assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test1@gmail.com", null, "google", "userid1").length); + assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test3@gmail.com", null, "google", "userid1").length); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUsersByAccountInfoForUnlinkedAccountsWithUnionOption() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test2@example.com"); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test3@example.com"); + AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(StorageLayer.getBaseStorage(process.getProcess())); + { + AuthRecipeUserInfo[] users = AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, true, "test1@example.com", "+919876543210", null, null); + assertEquals(2, users.length); + assertTrue(Arrays.asList(users).contains(user1)); + assertTrue(Arrays.asList(users).contains(user4)); + } + { + AuthRecipeUserInfo[] users = AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, true, "test1@example.com", null, "google", "userid1"); + assertEquals(2, users.length); + assertTrue(Arrays.asList(users).contains(user1)); + assertTrue(Arrays.asList(users).contains(user2)); + } + { + AuthRecipeUserInfo[] users = AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, true, null, "+919876543210", "google", "userid1"); + assertEquals(2, users.length); + assertTrue(Arrays.asList(users).contains(user4)); + assertTrue(Arrays.asList(users).contains(user2)); + } + { + AuthRecipeUserInfo[] users = AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, true, "test1@example.com", "+919876543210", "google", "userid1"); + assertEquals(3, users.length); + assertTrue(Arrays.asList(users).contains(user1)); + assertTrue(Arrays.asList(users).contains(user2)); + assertTrue(Arrays.asList(users).contains(user4)); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUnknownAccountInfo() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(StorageLayer.getBaseStorage(process.getProcess())); + assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test1@example.com", null, null, null).length); + assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, null, null, "google", "userid1").length); + assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, "test3@example.com", null, null, null).length); + assertEquals(0, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, null, "+919876543210", null, null).length); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUserByAccountInfoWhenAccountsAreLinked1() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = ThirdParty.signInUp(process.getProcess(), "google", "userid1", "test2@example.com").user; + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage( + StorageLayer.getBaseStorage(process.getProcess())); + + primaryUser = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId()); + + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test1@example.com", null, null, null)[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test2@example.com", null, null, null)[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + null, null, "google", "userid1")[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test1@example.com", null, "google", "userid1")[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test2@example.com", null, "google", "userid1")[0]); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUserByAccountInfoWhenAccountsAreLinked2() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password2"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage( + StorageLayer.getBaseStorage(process.getProcess())); + + primaryUser = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId()); + + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test1@example.com", null, null, null)[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test2@example.com", null, null, null)[0]); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUserByAccountInfoWhenAccountsAreLinked3() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createPasswordlessUserWithEmail(process.getProcess(), "test2@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage( + StorageLayer.getBaseStorage(process.getProcess())); + + primaryUser = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId()); + + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test1@example.com", null, null, null)[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test2@example.com", null, null, null)[0]); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUserByAccountInfoWhenAccountsAreLinked4() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage( + StorageLayer.getBaseStorage(process.getProcess())); + + primaryUser = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId()); + + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test1@example.com", null, null, null)[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + null, "+919876543210", null, null)[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test1@example.com", "+919876543210", null, null)[0]); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUserByAccountInfoWhenAccountsAreLinked5() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test2@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage( + StorageLayer.getBaseStorage(process.getProcess())); + + primaryUser = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId()); + + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test1@example.com", null, null, null)[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test2@example.com", null, null, null)[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + null, null, "google", "userid1")[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test1@example.com", null, "google", "userid1")[0]); + assertEquals(primaryUser, AuthRecipe.getUsersByAccountInfo(tenantIdentifierWithStorage, false, + "test2@example.com", null, "google", "userid1")[0]); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/GetUserByIdTest.java b/src/test/java/io/supertokens/test/accountlinking/GetUserByIdTest.java new file mode 100644 index 000000000..48bf9ee9a --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/GetUserByIdTest.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking; + +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.*; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.thirdparty.ThirdParty; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.*; + +public class GetUserByIdTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password) + throws DuplicateEmailException, StorageQueryException { + return EmailPassword.signUp(main, email, password); + } + + AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email) + throws EmailChangeNotAllowedException, StorageQueryException { + return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithPhone(Main main, String phone) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, null, phone, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + @Test + public void testAllLoginMethods() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + + assertFalse(user1.isPrimaryUser); + assertFalse(user2.isPrimaryUser); + assertFalse(user3.isPrimaryUser); + assertFalse(user4.isPrimaryUser); + + AuthRecipeUserInfo userToTest = AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId()); + assertNotNull(userToTest.getSupertokensUserId()); + assertFalse(userToTest.isPrimaryUser); + assertEquals(1, userToTest.loginMethods.length); + assertEquals("test@example.com", userToTest.loginMethods[0].email); + assertEquals(RECIPE_ID.EMAIL_PASSWORD, userToTest.loginMethods[0].recipeId); + assertEquals(user1.getSupertokensUserId(), userToTest.loginMethods[0].getSupertokensUserId()); + assertFalse(userToTest.loginMethods[0].verified); + assert(userToTest.loginMethods[0].timeJoined > 0); + + assertEquals(user1, AuthRecipe.getUserById(process.getProcess(), user1.getSupertokensUserId())); + assertEquals(user2, AuthRecipe.getUserById(process.getProcess(), user2.getSupertokensUserId())); + assertEquals(user3, AuthRecipe.getUserById(process.getProcess(), user3.getSupertokensUserId())); + assertEquals(user4, AuthRecipe.getUserById(process.getProcess(), user4.getSupertokensUserId())); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user4.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user1.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + for (String userId : new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId(), user3.getSupertokensUserId(), user4.getSupertokensUserId()}) { + AuthRecipeUserInfo result = AuthRecipe.getUserById(process.getProcess(), userId); + assertTrue(result.isPrimaryUser); + + assertEquals(4, result.loginMethods.length); + assertEquals(user1.loginMethods[0], result.loginMethods[0]); + assertEquals(user2.loginMethods[0], result.loginMethods[1]); + assertEquals(user3.loginMethods[0], result.loginMethods[2]); + assertEquals(user4.loginMethods[0], result.loginMethods[3]); + + assertEquals(user1.timeJoined, result.timeJoined); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUnknownUserIdReturnsNull() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + assertNull(AuthRecipe.getUserById(process.getProcess(), "unknownid")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testLoginMethodsAreSortedByTime() throws Exception { + for (int i = 0; i < 10; i++) { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + // Create users + AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password1"); + + // Link accounts randomly + String[] userIds = new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId(), user3.getSupertokensUserId(), user4.getSupertokensUserId()}; + Collections.shuffle(Arrays.asList(userIds)); + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), userIds[0]).user; + AuthRecipe.linkAccounts(process.getProcess(), userIds[1], primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), userIds[2], primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), userIds[3], primaryUser.getSupertokensUserId()); + + for (String userId : userIds) { + AuthRecipeUserInfo result = AuthRecipe.getUserById(process.getProcess(), userId); + assertTrue(result.isPrimaryUser); + + assertEquals(4, result.loginMethods.length); + assert(result.loginMethods[0].timeJoined <= result.loginMethods[1].timeJoined); + assert(result.loginMethods[1].timeJoined <= result.loginMethods[2].timeJoined); + assert(result.loginMethods[2].timeJoined <= result.loginMethods[3].timeJoined); + } + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java b/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java new file mode 100644 index 000000000..aab79a23a --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java @@ -0,0 +1,607 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; +import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException; +import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.session.Session; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.thirdparty.ThirdParty; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.assertNotNull; + + +public class LinkAccountsTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void linkAccountSuccess() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + Session.createNewSession(process.main, user2.getSupertokensUserId(), new JsonObject(), new JsonObject()); + String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId()); + assert (sessions.length == 1); + + boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked; + assert (!wasAlreadyLinked); + + AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId()); + AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId()); + assert (refetchUser2.equals(refetchUser)); + assert (refetchUser2.loginMethods.length == 2); + assert (refetchUser.loginMethods[0].equals(user.loginMethods[0])); + assert (refetchUser.loginMethods[1].equals(user2.loginMethods[0])); + assert (refetchUser.tenantIds.size() == 1); + assert (refetchUser.isPrimaryUser); + assert (refetchUser.getSupertokensUserId().equals(user.getSupertokensUserId())); + + // cause linkAccounts revokes sessions for the recipe user ID + sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId()); + assert (sessions.length == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkAccountSuccessWithSameEmail() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google", + "user-google", + "test@example.com"); + AuthRecipeUserInfo user2 = signInUpResponse.user; + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + Session.createNewSession(process.main, user2.getSupertokensUserId(), new JsonObject(), new JsonObject()); + String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId()); + assert (sessions.length == 1); + + boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked; + assert (!wasAlreadyLinked); + + AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId()); + AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId()); + assert (refetchUser2.equals(refetchUser)); + assert (refetchUser2.loginMethods.length == 2); + assert (refetchUser.loginMethods[0].equals(user.loginMethods[0])); + assert (refetchUser.loginMethods[1].equals(user2.loginMethods[0])); + assert (refetchUser.tenantIds.size() == 1); + assert (refetchUser.isPrimaryUser); + assert (refetchUser.getSupertokensUserId().equals(user.getSupertokensUserId())); + + // cause linkAccounts revokes sessions for the recipe user ID + sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId()); + assert (sessions.length == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatLinkingAccountsRequiresAccountLinkingFeatureToBeEnabled() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + try { + AuthRecipe.linkAccounts(process.main, "", ""); + assert (false); + } catch (FeatureNotEnabledException e) { + assert (e.getMessage() + .equals("Account linking feature is not enabled for this app. Please contact support to enable it" + + ".")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkAccountSuccessEvenIfUsingRecipeUserIdThatIsLinkedToPrimaryUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked; + assert (!wasAlreadyLinked); + + AuthRecipeUserInfo user3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", "password"); + assert (!user3.isPrimaryUser); + + wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user3.getSupertokensUserId(), user2.getSupertokensUserId()).wasAlreadyLinked; + assert (!wasAlreadyLinked); + + AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId()); + assert (refetchUser.loginMethods.length == 3); + assert (refetchUser.loginMethods[0].equals(user.loginMethods[0])); + assert (refetchUser.loginMethods[1].equals(user2.loginMethods[0])); + assert (refetchUser.loginMethods[2].equals(user3.loginMethods[0])); + assert (refetchUser.tenantIds.size() == 1); + assert (refetchUser.isPrimaryUser); + assert (refetchUser.getSupertokensUserId().equals(user.getSupertokensUserId())); + + AuthRecipeUserInfo refetchUser3 = AuthRecipe.getUserById(process.main, user3.getSupertokensUserId()); + assert (refetchUser3.equals(refetchUser)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void alreadyLinkAccountLinkAgain() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google", + "user-google", + "test@example.com"); + AuthRecipeUserInfo user2 = signInUpResponse.user; + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked; + assert (!wasAlreadyLinked); + + Session.createNewSession(process.main, user2.getSupertokensUserId(), new JsonObject(), new JsonObject()); + String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId()); + assert (sessions.length == 1); + + wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked; + assert (wasAlreadyLinked); + + // cause linkAccounts revokes sessions for the recipe user ID + sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId()); + assert (sessions.length == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkAccountFailureCauseRecipeUserIdLinkedWithAnotherPrimaryUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google", + "user-google", + "test@example.com"); + AuthRecipeUserInfo user2 = signInUpResponse.user; + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked; + assert (!wasAlreadyLinked); + + AuthRecipeUserInfo user3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", "password"); + assert (!user.isPrimaryUser); + AuthRecipe.createPrimaryUser(process.main, user3.getSupertokensUserId()); + + try { + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user3.getSupertokensUserId()); + assert (false); + } catch (RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException e) { + assert (e.recipeUser.getSupertokensUserId().equals(user.getSupertokensUserId())); + assert (e.getMessage().equals("The input recipe user ID is already linked to another user ID")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkAccountFailureInputUserIsNotAPrimaryUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google", + "user-google", + "test@example.com"); + AuthRecipeUserInfo user2 = signInUpResponse.user; + assert (!user2.isPrimaryUser); + + try { + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()); + assert (false); + } catch (InputUserIdIsNotAPrimaryUserException e) { + assert (e.userId.equals(user.getSupertokensUserId())); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkAccountFailureUserDoesNotExist() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google", + "user-google", + "test@example.com"); + AuthRecipeUserInfo user2 = signInUpResponse.user; + assert (!user2.isPrimaryUser); + + try { + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), "random"); + assert (false); + } catch (UnknownUserIdException e) { + } + + try { + AuthRecipe.linkAccounts(process.main, "random2", user.getSupertokensUserId()); + assert (false); + } catch (UnknownUserIdException e) { + } + + try { + AuthRecipe.linkAccounts(process.main, "random2", "random"); + assert (false); + } catch (UnknownUserIdException e) { + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkAccountFailureCauseAccountInfoAssociatedWithAPrimaryUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + Thread.sleep(50); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google", + "user-google", + "test@example.com"); + AuthRecipeUserInfo user2 = signInUpResponse.user; + assert (!user2.isPrimaryUser); + + AuthRecipeUserInfo otherPrimaryUser = EmailPassword.signUp(process.getProcess(), "test3@example.com", + "password"); + + AuthRecipe.createPrimaryUser(process.main, otherPrimaryUser.getSupertokensUserId()); + + try { + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), otherPrimaryUser.getSupertokensUserId()); + assert (false); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + assert (e.primaryUserId.equals(user.getSupertokensUserId())); + assert (e.getMessage().equals("This user's email is already associated with another user ID")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkAccountFailureCauseAccountInfoAssociatedWithAPrimaryUserEvenIfInDifferentTenant() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), + new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true), + new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(true), + new JsonObject())); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", + StorageLayer.getStorage(process.main)); + + AuthRecipeUserInfo user = EmailPassword.signUp(tenantIdentifierWithStorage, process.getProcess(), + "test@example.com", "password"); + assert (!user.isPrimaryUser); + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + Thread.sleep(50); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(tenantIdentifierWithStorage, + process.getProcess(), "google", + "user-google", + "test@example.com"); + AuthRecipeUserInfo user2 = signInUpResponse.user; + assert (!user2.isPrimaryUser); + + AuthRecipeUserInfo otherPrimaryUser = EmailPassword.signUp(process.getProcess(), "test3@example.com", + "password"); + + AuthRecipe.createPrimaryUser(process.main, otherPrimaryUser.getSupertokensUserId()); + + try { + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), otherPrimaryUser.getSupertokensUserId()); + assert (false); + } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { + assert (e.primaryUserId.equals(user.getSupertokensUserId())); + assert (e.getMessage().equals("This user's email is already associated with another user ID")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkAccountSuccessAcrossTenants() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), + new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true), + new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(true), + new JsonObject())); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", + StorageLayer.getStorage(process.main)); + + AuthRecipeUserInfo user = EmailPassword.signUp(tenantIdentifierWithStorage, process.getProcess(), + "test@example.com", "password"); + assert (!user.isPrimaryUser); + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + Thread.sleep(50); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp( + process.getProcess(), "google", + "user-google", + "test@example.com"); + AuthRecipeUserInfo user2 = signInUpResponse.user; + assert (!user2.isPrimaryUser); + + boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked; + assert (!wasAlreadyLinked); + + AuthRecipeUserInfo refetchedUser1 = AuthRecipe.getUserById(process.main, user.getSupertokensUserId()); + AuthRecipeUserInfo refetchedUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId()); + assert (refetchedUser1.getSupertokensUserId().equals(refetchedUser2.getSupertokensUserId())); + assert refetchedUser1.loginMethods.length == 2; + assert refetchedUser1.tenantIds.size() == 2; + assert refetchedUser1.tenantIds.contains("t1"); + assert refetchedUser1.tenantIds.contains("public"); + assert refetchedUser1.getSupertokensUserId().equals(user.getSupertokensUserId()); + assert refetchedUser1.isPrimaryUser; + assert refetchedUser1.loginMethods[0].getSupertokensUserId().equals(user.loginMethods[0].getSupertokensUserId()); + assert refetchedUser1.loginMethods[1].getSupertokensUserId().equals(user2.loginMethods[0].getSupertokensUserId()); + + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkAccountSuccessWithPasswordlessEmailAndPhoneNumber() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + Passwordless.CreateCodeResponse code = Passwordless.createCode(process.getProcess(), "u@e.com", null, null, + null); + Passwordless.ConsumeCodeResponse pResp = Passwordless.consumeCode(process.getProcess(), code.deviceId, + code.deviceIdHash, code.userInputCode, null); + AuthRecipeUserInfo user2 = pResp.user; + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + Passwordless.updateUser(process.main, user2.getSupertokensUserId(), null, new Passwordless.FieldUpdate("1234")); + user2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId()); + + boolean wasAlreadyLinked = AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()).wasAlreadyLinked; + assert (!wasAlreadyLinked); + + AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId()); + AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId()); + assert (refetchUser2.equals(refetchUser)); + assert (refetchUser2.loginMethods.length == 2); + assert (refetchUser.loginMethods[0].equals(user.loginMethods[0])); + assert (refetchUser.loginMethods[1].equals(user2.loginMethods[0])); + assert (refetchUser.tenantIds.size() == 1); + assert (refetchUser.isPrimaryUser); + assert (refetchUser.getSupertokensUserId().equals(user.getSupertokensUserId())); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java new file mode 100644 index 000000000..e9f34f4d6 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java @@ -0,0 +1,1051 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking; + +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; +import io.supertokens.authRecipe.exception.RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.emailpassword.exceptions.WrongCredentialsException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.exception.*; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.PhoneNumberChangeNotAllowedException; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.exceptions.InvalidConfigException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException; +import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.thirdparty.InvalidProviderConfigException; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.userroles.UserRoles; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +public class MultitenantTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + TenantIdentifier t1, t2, t3, t4; + + private void createTenants(Main main) + throws StorageQueryException, TenantOrAppNotFoundException, InvalidProviderConfigException, + FeatureNotEnabledException, IOException, InvalidConfigException, + CannotModifyBaseConfigException, BadPermissionException { + // User pool 1 - (null, a1, null) + // User pool 2 - (null, a1, t1), (null, a1, t2) + + { // tenant 1 + JsonObject config = new JsonObject(); + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", null); + + StorageLayer.getStorage(new TenantIdentifier(null, null, null), main) + .modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant( + main, + new TenantIdentifier(null, null, null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ) + ); + } + + { // tenant 2 + JsonObject config = new JsonObject(); + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", "t1"); + + StorageLayer.getStorage(new TenantIdentifier(null, null, null), main) + .modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant( + main, + new TenantIdentifier(null, "a1", null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ) + ); + } + + { // tenant 3 + JsonObject config = new JsonObject(); + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", "t2"); + + StorageLayer.getStorage(new TenantIdentifier(null, null, null), main) + .modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant( + main, + new TenantIdentifier(null, "a1", null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ) + ); + } + + { // tenant 4 + JsonObject config = new JsonObject(); + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", "t3"); + + StorageLayer.getStorage(new TenantIdentifier(null, null, null), main) + .modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant( + main, + new TenantIdentifier(null, "a1", null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ) + ); + } + } + + @Test + public void testUserAreNotAutomaticallySharedBetweenTenantsOfLinkedAccountsForPless() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(process.getProcess()); + + t1 = new TenantIdentifier(null, "a1", null); + t2 = new TenantIdentifier(null, "a1", "t1"); + + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test@example.com", "password"); + Passwordless.CreateCodeResponse user2Code = Passwordless.createCode(t1WithStorage, process.getProcess(), + "test@example.com", null, null, null); + AuthRecipeUserInfo user2 = Passwordless.consumeCode(t1WithStorage, process.getProcess(), user2Code.deviceId, user2Code.deviceIdHash, user2Code.userInputCode, null).user; + + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user1.getSupertokensUserId()); + + { // user2 should not be shared in tenant2 + Passwordless.CreateCodeResponse user3Code = Passwordless.createCode(t2WithStorage, process.getProcess(), + "test@example.com", null, null, null); + Passwordless.ConsumeCodeResponse res = Passwordless.consumeCode(t2WithStorage, process.getProcess(), + user3Code.deviceId, user3Code.deviceIdHash, user3Code.userInputCode, null); + assertTrue(res.createdNewUser); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUserAreNotAutomaticallySharedBetweenTenantsOfLinkedAccountsForTP() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(process.getProcess()); + + t1 = new TenantIdentifier(null, "a1", null); + t2 = new TenantIdentifier(null, "a1", "t1"); + + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test@example.com", "password"); + AuthRecipeUserInfo user2 = ThirdParty.signInUp(t1WithStorage, process.getProcess(), "google", "googleid1", "test@example.com").user; + + AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user1.getSupertokensUserId()); + + { // user2 should not be shared in tenant2 + ThirdParty.SignInUpResponse res = ThirdParty.signInUp(t2WithStorage, process.getProcess(), "google", + "googleid1", "test@example.com"); + assertTrue(res.createdNewUser); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testTenantDeletionWithAccountLinking() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(process.getProcess()); + + t1 = new TenantIdentifier(null, "a1", null); + t2 = new TenantIdentifier(null, "a1", "t1"); + + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test@example.com", "password"); + AuthRecipeUserInfo user2 = ThirdParty.signInUp(t2WithStorage, process.getProcess(), "google", "googleid1", "test@example.com").user; + + AuthRecipe.createPrimaryUser(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + Multitenancy.deleteTenant(t2, process.getProcess()); + + AuthRecipeUserInfo getUser1 = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + for (LoginMethod lm : getUser1.loginMethods) { + assertEquals(0, lm.tenantIds.size()); + } + + AuthRecipeUserInfo getUser2 = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId()); + for (LoginMethod lm : getUser2.loginMethods) { + assertEquals(0, lm.tenantIds.size()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testTenantDeletionWithAccountLinkingWithUserRoles() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(process.getProcess()); + + t1 = new TenantIdentifier(null, "a1", null); + t2 = new TenantIdentifier(null, "a1", "t1"); + + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); + TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); + + AuthRecipeUserInfo user1 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test@example.com", "password"); + AuthRecipeUserInfo user2 = ThirdParty.signInUp(t2WithStorage, process.getProcess(), "google", "googleid1", "test@example.com").user; + + AuthRecipe.createPrimaryUser(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + UserRoles.createNewRoleOrModifyItsPermissions(t2WithStorage.toAppIdentifierWithStorage(), "admin", new String[]{"p1"}); + UserRoles.addRoleToUser(t2WithStorage, user1.getSupertokensUserId(), "admin"); + + Multitenancy.deleteTenant(t2, process.getProcess()); + + createTenants(process.getProcess()); // create the tenant again + + Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user1.getSupertokensUserId()); // add the user to the tenant again + Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user2.getSupertokensUserId()); // add the user to the tenant again + + AuthRecipeUserInfo getUser1 = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); + for (LoginMethod lm : getUser1.loginMethods) { + assertEquals(1, lm.tenantIds.size()); + } + + AuthRecipeUserInfo getUser2 = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId()); + for (LoginMethod lm : getUser2.loginMethods) { + assertEquals(1, lm.tenantIds.size()); + } + + String[] roles = UserRoles.getRolesForUser(t2WithStorage, user1.getSupertokensUserId()); + assertEquals(0, roles.length); // must be deleted with tenant + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testVariousCases() throws Exception { + t1 = new TenantIdentifier(null, "a1", null); + t2 = new TenantIdentifier(null, "a1", "t1"); + t3 = new TenantIdentifier(null, "a1", "t2"); + t4 = new TenantIdentifier(null, "a1", "t3"); + + TestCase[] testCases = new TestCase[]{ + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test@example.com"), + new CreatePlessUserWithEmail(t2, "test@example.com"), + new MakePrimaryUser(t1, 0), + new AssociateUserToTenant(t2, 0), + }), + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test@example.com"), + new CreatePlessUserWithEmail(t2, "test@example.com"), + new AssociateUserToTenant(t2, 0), + new MakePrimaryUser(t1, 0), + }), + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test@example.com"), + new CreatePlessUserWithEmail(t2, "test@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new SignInEmailPasswordUser(t2, 0).expect(new WrongCredentialsException()) + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t1, 2).expect(new DuplicateEmailException()), + new AssociateUserToTenant(t2, 2), // Allowed + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t2, 2), + new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t1, 2).expect(new DuplicateEmailException()), + new AssociateUserToTenant(t2, 2), // Allowed + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t2, 2), + new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t1, 2), + new AssociateUserToTenant(t2, 2), // Allowed + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t2, 2), + new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t2, "test2@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new UpdatePlessUserEmail(t1, 0, "test2@example.com"), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t1, "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdatePlessUserEmail(t1, 0, "test3@example.com").expect(new EmailChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t1, "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdatePlessUserEmail(t1, 1, "test1@example.com").expect(new EmailChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithPhone(t1, "+1000001"), + new CreatePlessUserWithPhone(t1, "+1000003"), + new CreatePlessUserWithPhone(t2, "+1000002"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdatePlessUserPhone(t1, 0, "+1000003").expect(new PhoneNumberChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithPhone(t1, "+1000001"), + new CreatePlessUserWithPhone(t1, "+1000003"), + new CreatePlessUserWithPhone(t2, "+1000002"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdatePlessUserPhone(t1, 1, "+1000001").expect(new PhoneNumberChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateEmailPasswordUser(t1, "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdateEmailPasswordUserEmail(t1, 0, "test3@example.com").expect(new EmailChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateEmailPasswordUser(t1, "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdateEmailPasswordUserEmail(t1, 1, "test1@example.com").expect(new EmailChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid", "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t1, 2).expect(new DuplicateEmailException()), + new AssociateUserToTenant(t2, 2), // Allowed + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid", "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid", "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t2, 2), + new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid", "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t1, 2), + new AssociateUserToTenant(t2, 2), // Allowed + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid", "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid", "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t2, 2), + new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid2", "test2@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new CreateThirdPartyUser(t1, "google", "googleid1", "test2@example.com"), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"), + new CreateThirdPartyUser(t1, "google", "googleid3", "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new CreateThirdPartyUser(t1, "google", "googleid1", "test3@example.com").expect(new EmailChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"), + new CreateThirdPartyUser(t1, "google", "googleid3", "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new CreateThirdPartyUser(t1, "google", "googleid3", "test1@example.com").expect(new EmailChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithPhone(t1, "+1000001"), + new CreatePlessUserWithPhone(t2, "+1000002"), + new CreatePlessUserWithPhone(t3, "+1000001"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t1, 2).expect(new DuplicatePhoneNumberException()), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithPhoneNumberAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid2", "test2@example.com"), + new CreateThirdPartyUser(t3, "google", "googleid1", "test3@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t1, 2).expect(new DuplicateThirdPartyUserException()), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException("")), + }), + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid2", "test2@example.com"), + new CreateThirdPartyUser(t1, "google", "googleid3", "test3@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new MakePrimaryUser(t1, 2), + new CreateThirdPartyUser(t1, "google", "googleid1", "test3@example.com").expect(new EmailChangeNotAllowedException()), + new CreateThirdPartyUser(t1, "google", "googleid3", "test1@example.com").expect(new EmailChangeNotAllowedException()), + }), + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test@example.com"), + new CreateEmailPasswordUser(t1, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new UnlinkAccount(t1, 0), + new AssociateUserToTenant(t2, 0).expect(new UnknownUserIdException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test@example.com"), + new CreatePlessUserWithEmail(t1, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new UnlinkAccount(t1, 0), + new AssociateUserToTenant(t2, 0).expect(new UnknownUserIdException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid1", "test@example.com"), + new CreateThirdPartyUser(t1, "google", "googleid2", "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new UnlinkAccount(t1, 0), + new AssociateUserToTenant(t2, 0).expect(new UnknownUserIdException()), + }), + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test@example.com"), + new CreateEmailPasswordUser(t1, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new DisassociateUserFromTenant(t1, 0), + new AssociateUserToTenant(t2, 0), + new TestCaseStep() { + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, main)); + AuthRecipeUserInfo user = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), TestCase.users.get(0).getSupertokensUserId()); + assertEquals(2, user.loginMethods.length); + assertTrue(user.loginMethods[0].tenantIds.contains(t2.getTenantId())); + assertTrue(user.loginMethods[1].tenantIds.contains(t1.getTenantId())); + } + } + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test@example.com"), + new DisassociateUserFromTenant(t1, 0), + new CreateEmailPasswordUser(t1, "test@example.com"), + new DisassociateUserFromTenant(t1, 1), + new MakePrimaryUser(t1, 0), + new MakePrimaryUser(t1, 1), + new AssociateUserToTenant(t1, 0), + new AssociateUserToTenant(t1, 1).expect(new DuplicateEmailException()), + new LinkAccounts(t1, 0, 1).expect(new RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException(null, "")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test@example.com"), + new DisassociateUserFromTenant(t1, 0), + new CreateEmailPasswordUser(t1, "test@example.com"), + new DisassociateUserFromTenant(t1, 1), + new MakePrimaryUser(t1, 0), + new AssociateUserToTenant(t1, 0), + new LinkAccounts(t1, 0, 1), + new AssociateUserToTenant(t1, 1).expect(new DuplicateEmailException()), + new AssociateUserToTenant(t2, 1), + }), + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateEmailPasswordUser(t1, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new UnlinkAccount(t1, 0), + new TestCaseStep() { + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, main)); + AuthRecipe.deleteUser(t1WithStorage.toAppIdentifierWithStorage(), TestCase.users.get(1).getSupertokensUserId()); + } + }, + new TestCaseStep() { + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, main)); + AuthRecipeUserInfo user = AuthRecipe.getUserById(t1WithStorage.toAppIdentifierWithStorage(), TestCase.users.get(0).getSupertokensUserId()); + assertNull(user); + } + } + }), + }; + + int i = 0; + for (TestCase testCase : testCases) { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(process.getProcess()); + + System.out.println("Executing test case : " + i); + testCase.doTest(process.getProcess()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + i++; + } + } + + private static class TestCase { + TestCaseStep[] steps; + public static List users; + + public static void resetUsers() { + users = new ArrayList<>(); + } + + public static void addUser(AuthRecipeUserInfo user) { + users.add(user); + } + + public TestCase(TestCaseStep[] steps) { + this.steps = steps; + } + + public void doTest(Main main) throws Exception { + TestCase.resetUsers(); + + for (TestCaseStep step : steps) { + step.doStep(main); + } + } + } + + private static abstract class TestCaseStep { + Exception e; + + public TestCaseStep expect(Exception e) { + this.e = e; + return this; + } + + public void doStep(Main main) throws Exception { + if (e == null) { + this.execute(main); + } else { + try { + this.execute(main); + fail(); + } catch (Exception e) { + assertEquals(this.e.getClass(), e.getClass()); + } + } + } + + abstract public void execute(Main main) throws Exception; + } + + private static class CreateEmailPasswordUser extends TestCaseStep { + private final TenantIdentifier tenantIdentifier; + private final String email; + + public CreateEmailPasswordUser(TenantIdentifier tenantIdentifier, String email) { + this.tenantIdentifier = tenantIdentifier; + this.email = email; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + AuthRecipeUserInfo user = EmailPassword.signUp(tenantIdentifierWithStorage, main, email, "password"); + TestCase.addUser(user); + } + } + + private static class CreatePlessUserWithEmail extends TestCaseStep { + private final TenantIdentifier tenantIdentifier; + private final String email; + + public CreatePlessUserWithEmail(TenantIdentifier tenantIdentifier, String email) { + this.tenantIdentifier = tenantIdentifier; + this.email = email; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + Passwordless.CreateCodeResponse code = Passwordless.createCode(tenantIdentifierWithStorage, main, + email, null, null, null); + AuthRecipeUserInfo user = Passwordless.consumeCode(tenantIdentifierWithStorage, main, code.deviceId, code.deviceIdHash, code.userInputCode, null).user; + TestCase.addUser(user); + } + } + + private static class CreatePlessUserWithPhone extends TestCaseStep { + private final TenantIdentifier tenantIdentifier; + private final String phoneNumber; + + public CreatePlessUserWithPhone(TenantIdentifier tenantIdentifier, String phoneNumber) { + this.tenantIdentifier = tenantIdentifier; + this.phoneNumber = phoneNumber; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + Passwordless.CreateCodeResponse code = Passwordless.createCode(tenantIdentifierWithStorage, main, + null, phoneNumber, null, null); + AuthRecipeUserInfo user = Passwordless.consumeCode(tenantIdentifierWithStorage, main, code.deviceId, code.deviceIdHash, code.userInputCode, null).user; + TestCase.addUser(user); + } + } + + private static class CreateThirdPartyUser extends TestCaseStep { + TenantIdentifier tenantIdentifier; + String thirdPartyId; + String thirdPartyUserId; + String email; + + public CreateThirdPartyUser(TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId, String email) { + this.tenantIdentifier = tenantIdentifier; + this.thirdPartyId = thirdPartyId; + this.thirdPartyUserId = thirdPartyUserId; + this.email = email; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + AuthRecipeUserInfo user = ThirdParty.signInUp(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, email).user; + TestCase.addUser(user); + } + } + + private static class MakePrimaryUser extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + + public MakePrimaryUser(TenantIdentifier tenantIdentifier, int userIndex) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + AuthRecipe.createPrimaryUser(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId()); + } + } + + private static class LinkAccounts extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int primaryUserIndex; + int recipeUserIndex; + + public LinkAccounts(TenantIdentifier tenantIdentifier, int primaryUserIndex, int recipeUserIndex) { + this.tenantIdentifier = tenantIdentifier; + this.primaryUserIndex = primaryUserIndex; + this.recipeUserIndex = recipeUserIndex; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + AuthRecipe.linkAccounts(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(recipeUserIndex).getSupertokensUserId(), TestCase.users.get(primaryUserIndex).getSupertokensUserId()); + } + } + + private static class AssociateUserToTenant extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + + public AssociateUserToTenant(TenantIdentifier tenantIdentifier, int userIndex) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + Multitenancy.addUserIdToTenant(main, tenantIdentifierWithStorage, TestCase.users.get(userIndex).getSupertokensUserId()); + } + } + + private static class UpdateEmailPasswordUserEmail extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + String email; + + public UpdateEmailPasswordUserEmail(TenantIdentifier tenantIdentifier, int userIndex, String email) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + this.email = email; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + EmailPassword.updateUsersEmailOrPassword(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main, TestCase.users.get(userIndex).getSupertokensUserId(), email, null); + } + } + + private static class UpdatePlessUserEmail extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + String email; + + public UpdatePlessUserEmail(TenantIdentifier tenantIdentifier, int userIndex, String email) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + this.email = email; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + Passwordless.updateUser(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId(), new Passwordless.FieldUpdate(email), null); + } + } + + private static class UpdatePlessUserPhone extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + String phoneNumber; + + public UpdatePlessUserPhone(TenantIdentifier tenantIdentifier, int userIndex, String phoneNumber) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + this.phoneNumber = phoneNumber; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + Passwordless.updateUser(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId(), null, new Passwordless.FieldUpdate(phoneNumber)); + } + } + + private static class UnlinkAccount extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + + public UnlinkAccount(TenantIdentifier tenantIdentifier, int userIndex) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + AuthRecipe.unlinkAccounts(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId()); + } + } + + private static class SignInEmailPasswordUser extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + + public SignInEmailPasswordUser(TenantIdentifier tenantIdentifier, int userIndex) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + EmailPassword.signIn(tenantIdentifierWithStorage, main, TestCase.users.get(userIndex).loginMethods[0].email, "password"); + } + } + + private static class DisassociateUserFromTenant extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + + public DisassociateUserFromTenant(TenantIdentifier tenantIdentifier, int userIndex) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + Multitenancy.removeUserIdFromTenant(main, tenantIdentifierWithStorage, TestCase.users.get(userIndex).getSupertokensUserId(), null); + } + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/UnlinkAccountsTest.java b/src/test/java/io/supertokens/test/accountlinking/UnlinkAccountsTest.java new file mode 100644 index 000000000..5a9d7cb74 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/UnlinkAccountsTest.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.authRecipe.exception.InputUserIdIsNotAPrimaryUserException; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.session.Session; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + + +public class UnlinkAccountsTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void unlinkAccountSuccess() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()); + + Session.createNewSession(process.main, user2.getSupertokensUserId(), new JsonObject(), new JsonObject()); + String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId()); + assert (sessions.length == 1); + + boolean didDelete = AuthRecipe.unlinkAccounts(process.main, user2.getSupertokensUserId()); + assert (!didDelete); + + AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId()); + assert (!refetchUser2.isPrimaryUser); + assert (refetchUser2.getSupertokensUserId().equals(user2.getSupertokensUserId())); + assert (refetchUser2.loginMethods.length == 1); + assert (refetchUser2.loginMethods[0].getSupertokensUserId().equals(user2.getSupertokensUserId())); + + AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId()); + assert (!refetchUser2.equals(refetchUser)); + assert (refetchUser.isPrimaryUser); + assert (refetchUser.loginMethods.length == 1); + assert (refetchUser.loginMethods[0].getSupertokensUserId().equals(user.getSupertokensUserId())); + + // cause linkAccounts revokes sessions for the recipe user ID + sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId()); + assert (sessions.length == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void unlinkAccountWithoutPrimaryUserId() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + try { + AuthRecipe.unlinkAccounts(process.main, user.getSupertokensUserId()); + assert (false); + } catch (InputUserIdIsNotAPrimaryUserException e) { + assert (e.userId.equals(user.getSupertokensUserId())); + } + + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void unlinkAccountWithUnknownUserId() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + try { + AuthRecipe.unlinkAccounts(process.main, "random"); + assert (false); + } catch (UnknownUserIdException e) { + } + + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void unlinkAccountWithPrimaryUserBecomesRecipeUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + Session.createNewSession(process.main, user.getSupertokensUserId(), new JsonObject(), new JsonObject()); + String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user.getSupertokensUserId()); + assert (sessions.length == 1); + + boolean didDelete = AuthRecipe.unlinkAccounts(process.main, user.getSupertokensUserId()); + assert (!didDelete); + + AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId()); + assert (!refetchUser.isPrimaryUser); + assert (refetchUser.loginMethods.length == 1); + assert (refetchUser.loginMethods[0].getSupertokensUserId().equals(user.getSupertokensUserId())); + + // cause linkAccounts revokes sessions for the recipe user ID + sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user.getSupertokensUserId()); + assert (sessions.length == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void unlinkAccountSuccessButDeletesUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()); + + Session.createNewSession(process.main, user.getSupertokensUserId(), new JsonObject(), new JsonObject()); + String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user.getSupertokensUserId()); + assert (sessions.length == 1); + + boolean didDelete = AuthRecipe.unlinkAccounts(process.main, user.getSupertokensUserId()); + assert (didDelete); + + AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId()); + assert (refetchUser2.isPrimaryUser); + assert (refetchUser2.getSupertokensUserId().equals(user.getSupertokensUserId())); + assert (refetchUser2.loginMethods.length == 1); + assert (refetchUser2.loginMethods[0].getSupertokensUserId().equals(user2.getSupertokensUserId())); + + AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId()); + assert (refetchUser2.equals(refetchUser)); + + // cause linkAccounts revokes sessions for the recipe user ID + sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user.getSupertokensUserId()); + assert (sessions.length == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUnlinkUserDeletesRecipeUserAndAnotherUserLinkToIt() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + AuthRecipeUserInfo user3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", "password"); + + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + AuthRecipe.unlinkAccounts(process.getProcess(), user1.getSupertokensUserId()); + + AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.getProcess(), user2.getSupertokensUserId()); + assertEquals(refetchUser2.getSupertokensUserId(), user1.getSupertokensUserId()); + + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), user2.getSupertokensUserId()); + AuthRecipeUserInfo refetchUser3 = AuthRecipe.getUserById(process.getProcess(), user3.getSupertokensUserId()); + assertEquals(refetchUser3.getSupertokensUserId(), user1.getSupertokensUserId()); + + assertEquals(refetchUser3.loginMethods.length, 2); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/UpdateUserTest.java b/src/test/java/io/supertokens/test/accountlinking/UpdateUserTest.java new file mode 100644 index 000000000..899ca6a6a --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/UpdateUserTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking; + +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.thirdparty.ThirdParty; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + +public class UpdateUserTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testThatUpdateEmailFailsWhenPassedWithNonEmailPasswordRecipeUserId() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user2 = ThirdParty.signInUp(process.getProcess(), "google", "googleid1", "test2@example.com").user; + + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + try { + EmailPassword.updateUsersEmailOrPassword(process.getProcess(), user2.getSupertokensUserId(), "test3@example.com", null); + fail(); + } catch (UnknownUserIdException e) { + // ignore + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java b/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java new file mode 100644 index 000000000..10c652c3c --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking.api; + +import com.google.gson.JsonObject; +import io.supertokens.ActiveUsers; +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.*; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.utils.SemVer; +import io.supertokens.webserver.WebserverAPI; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class ActiveUserTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + + AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password) + throws DuplicateEmailException, StorageQueryException { + return EmailPassword.signUp(main, email, password); + } + + AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email) + throws EmailChangeNotAllowedException, StorageQueryException { + return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithPhone(Main main, String phone) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, null, phone, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + @Test + public void testActiveUserIsRemovedAfterLinkingAccounts() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password"); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "google-user", "test@example.com"); + + { + // Update active user + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "test@example.com"); + responseBody.addProperty("password", "password"); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + } + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + } + + int userCount = ActiveUsers.countUsersActiveSince(process.getProcess(), System.currentTimeMillis() - 10000); + assertEquals(2, userCount); + + { + // Link accounts + AuthRecipe.createPrimaryUser(process.main, user2.getSupertokensUserId()); + + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user1.getSupertokensUserId()); + params.addProperty("primaryUserId", user2.getSupertokensUserId()); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + } + + // we don't remove the active user for the recipe user, so it should still be 2 + userCount = ActiveUsers.countUsersActiveSince(process.getProcess(), System.currentTimeMillis() - 10000); + assertEquals(2, userCount); + + // Sign in to the accounts once again + { + // Update active user + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "test@example.com"); + responseBody.addProperty("password", "password"); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + } + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + } + + // there should still be only one active user + userCount = ActiveUsers.countUsersActiveSince(process.getProcess(), System.currentTimeMillis() - 10000); + assertEquals(2, userCount); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/api/CanCreatePrimaryUserAPITest.java b/src/test/java/io/supertokens/test/accountlinking/api/CanCreatePrimaryUserAPITest.java new file mode 100644 index 000000000..bf2d42429 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/api/CanCreatePrimaryUserAPITest.java @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.httpRequest.HttpResponseException; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.webserver.WebserverAPI; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class CanCreatePrimaryUserAPITest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void canCreateReturnsTrue() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + } + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void canCreateReturnsTrueWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + UserIdMapping.createUserIdMapping(process.main, user.getSupertokensUserId(), "r1", null, false); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", "r1"); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + } + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", "r1"); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void canCreatePrimaryUserBadInput() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + Map params = new HashMap<>(); + + try { + HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Field name 'recipeUserId' is missing in GET " + + "request")); + } + } + + { + Map params = new HashMap<>(); + params.put("recipeUserId", "random"); + + try { + HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Unknown user ID provided")); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makePrimaryUserFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test@example.com"); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", signInUpResponse.user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals(emailPasswordUser.getSupertokensUserId(), response.get("primaryUserId").getAsString()); + assertEquals("This user's email is already associated with another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makingPrimaryUserFailsCauseAlreadyLinkedToAnotherAccount() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser1 = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + AuthRecipeUserInfo emailPasswordUser2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, emailPasswordUser2.getSupertokensUserId(), emailPasswordUser1.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", emailPasswordUser2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals(emailPasswordUser1.getSupertokensUserId(), response.get("primaryUserId").getAsString()); + assertEquals("This user ID is already linked to another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makePrimaryUserFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUserWithUserIdMapping() + throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser.getSupertokensUserId(), "r1", null, false); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test@example.com"); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", signInUpResponse.user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals("r1", response.get("primaryUserId").getAsString()); + assertEquals("This user's email is already associated with another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makingPrimaryUserFailsCauseAlreadyLinkedToAnotherAccountWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser1 = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser1.getSupertokensUserId(), "r1", null, false); + AuthRecipeUserInfo emailPasswordUser2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, emailPasswordUser2.getSupertokensUserId(), emailPasswordUser1.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", emailPasswordUser2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals("r1", response.get("primaryUserId").getAsString()); + assertEquals("This user ID is already linked to another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + +} diff --git a/src/test/java/io/supertokens/test/accountlinking/api/CanLinkAccountsAPITest.java b/src/test/java/io/supertokens/test/accountlinking/api/CanLinkAccountsAPITest.java new file mode 100644 index 000000000..ab155e481 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/api/CanLinkAccountsAPITest.java @@ -0,0 +1,489 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.httpRequest.HttpResponseException; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.webserver.WebserverAPI; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class CanLinkAccountsAPITest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void canLinkReturnsTrue() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "abcd1234"); + + AuthRecipe.createPrimaryUser(process.main, user2.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", user.getSupertokensUserId()); + params.put("primaryUserId", user2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("accountsAlreadyLinked").getAsBoolean()); + } + + AuthRecipe.linkAccounts(process.main, user.getSupertokensUserId(), user2.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", user.getSupertokensUserId()); + params.put("primaryUserId", user2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("accountsAlreadyLinked").getAsBoolean()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void canLinkReturnsTrueWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + UserIdMapping.createUserIdMapping(process.main, user.getSupertokensUserId(), "r1", null, false); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "abcd1234"); + UserIdMapping.createUserIdMapping(process.main, user2.getSupertokensUserId(), "r2", null, false); + + AuthRecipe.createPrimaryUser(process.main, user2.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", "r1"); + params.put("primaryUserId", "r2"); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("accountsAlreadyLinked").getAsBoolean()); + } + + AuthRecipe.linkAccounts(process.main, user.getSupertokensUserId(), user2.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", "r1"); + params.put("primaryUserId", "r2"); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("accountsAlreadyLinked").getAsBoolean()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void canLinkUserBadInput() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + Map params = new HashMap<>(); + + try { + HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Field name 'recipeUserId' is missing in GET " + + "request")); + } + } + + { + Map params = new HashMap<>(); + params.put("recipeUserId", "random"); + + try { + HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Field name 'primaryUserId' is missing in GET " + + "request")); + } + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "abcd1234"); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", user2.getSupertokensUserId()); + params.put("primaryUserId", "random"); + + try { + HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Unknown user ID provided")); + } + } + + { + Map params = new HashMap<>(); + params.put("recipeUserId", "random"); + params.put("primaryUserId", user.getSupertokensUserId()); + + try { + HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Unknown user ID provided")); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkingUsersFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test2@example.com"); + + AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId()); + + ThirdParty.SignInUpResponse signInUpResponse2 = ThirdParty.signInUp(process.main, "fb", "user-fb", + "test@example.com"); + + + { + Map params = new HashMap<>(); + params.put("primaryUserId", signInUpResponse.user.getSupertokensUserId()); + params.put("recipeUserId", signInUpResponse2.user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals(emailPasswordUser.getSupertokensUserId(), response.get("primaryUserId").getAsString()); + assertEquals("This user's email is already associated with another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkingUsersFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUserWithUserIdMapping() + throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser.getSupertokensUserId(), "e1", null, false); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test2@example.com"); + UserIdMapping.createUserIdMapping(process.main, signInUpResponse.user.getSupertokensUserId(), "e2", null, false); + + AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId()); + + ThirdParty.SignInUpResponse signInUpResponse2 = ThirdParty.signInUp(process.main, "fb", "user-fb", + "test@example.com"); + UserIdMapping.createUserIdMapping(process.main, signInUpResponse2.user.getSupertokensUserId(), "e3", null, false); + + + { + Map params = new HashMap<>(); + params.put("primaryUserId", "e2"); + params.put("recipeUserId", "e3"); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals("e1", response.get("primaryUserId").getAsString()); + assertEquals("This user's email is already associated with another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkingUserFailsCauseAlreadyLinkedToAnotherAccount() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser1 = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + AuthRecipeUserInfo emailPasswordUser2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, emailPasswordUser2.getSupertokensUserId(), emailPasswordUser1.getSupertokensUserId()); + + AuthRecipeUserInfo emailPasswordUser3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser3.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", emailPasswordUser2.getSupertokensUserId()); + params.put("primaryUserId", emailPasswordUser3.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals(emailPasswordUser1.getSupertokensUserId(), response.get("primaryUserId").getAsString()); + assertEquals("The input recipe user ID is already linked to another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makingPrimaryUserFailsCauseAlreadyLinkedToAnotherAccountWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser1 = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser1.getSupertokensUserId(), "r1", null, false); + AuthRecipeUserInfo emailPasswordUser2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser2.getSupertokensUserId(), "r2", null, false); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, emailPasswordUser2.getSupertokensUserId(), emailPasswordUser1.getSupertokensUserId()); + + AuthRecipeUserInfo emailPasswordUser3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser3.getSupertokensUserId(), "r3", null, false); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser3.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", "r2"); + params.put("primaryUserId", "r3"); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals("r1", response.get("primaryUserId").getAsString()); + assertEquals("The input recipe user ID is already linked to another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void inputUserIsNotAPrimaryUserTest() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "abcd1234"); + + { + Map params = new HashMap<>(); + params.put("recipeUserId", user.getSupertokensUserId()); + params.put("primaryUserId", user2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link/check", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(1, response.entrySet().size()); + assertEquals("INPUT_USER_IS_NOT_A_PRIMARY_USER", response.get("status").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + +} diff --git a/src/test/java/io/supertokens/test/accountlinking/api/CreatePrimaryUserAPITest.java b/src/test/java/io/supertokens/test/accountlinking/api/CreatePrimaryUserAPITest.java new file mode 100644 index 000000000..ce4dd6010 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/api/CreatePrimaryUserAPITest.java @@ -0,0 +1,555 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.multitenancy.*; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.httpRequest.HttpResponseException; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.webserver.WebserverAPI; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class CreatePrimaryUserAPITest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void createReturnsSucceeds() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + JsonObject userObj; + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + + // check user object + JsonObject jsonUser = response.get("user").getAsJsonObject(); + assert (jsonUser.get("id").getAsString().equals(user.getSupertokensUserId())); + assert (jsonUser.get("timeJoined").getAsLong() == user.timeJoined); + assert (jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 1); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("test@example.com")); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 0); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 1); + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), user.getSupertokensUserId()); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "test@example.com"); + assert (lM.entrySet().size() == 6); + userObj = jsonUser; + } + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + assertEquals(response.get("user"), userObj); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void createReturnsTrueWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + UserIdMapping.createUserIdMapping(process.main, user.getSupertokensUserId(), "r1", null, false); + + JsonObject userObj; + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", "r1"); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + // check user object + JsonObject jsonUser = response.get("user").getAsJsonObject(); + assert (jsonUser.get("id").getAsString().equals("r1")); + assert (jsonUser.get("timeJoined").getAsLong() == user.timeJoined); + assert (jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 1); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("test@example.com")); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 0); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 1); + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), "r1"); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "test@example.com"); + assert (lM.entrySet().size() == 6); + userObj = jsonUser; + } + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", "r1"); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + assertEquals(response.get("user"), userObj); + } + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + assertEquals(response.get("user"), userObj); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void createPrimaryUserBadInput() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + Map params = new HashMap<>(); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", new JsonObject(), 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Field name 'recipeUserId' is invalid in JSON " + + "input")); + } + } + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", "random"); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Unknown user ID provided")); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makePrimaryUserFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test@example.com"); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", signInUpResponse.user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals(emailPasswordUser.getSupertokensUserId(), response.get("primaryUserId").getAsString()); + assertEquals("This user's email is already associated with another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makingPrimaryUserFailsCauseAlreadyLinkedToAnotherAccount() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser1 = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + AuthRecipeUserInfo emailPasswordUser2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, emailPasswordUser2.getSupertokensUserId(), emailPasswordUser1.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", emailPasswordUser2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals(emailPasswordUser1.getSupertokensUserId(), response.get("primaryUserId").getAsString()); + assertEquals("This user ID is already linked to another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makePrimaryUserFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUserWithUserIdMapping() + throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser.getSupertokensUserId(), "r1", null, false); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test@example.com"); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", signInUpResponse.user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals("r1", response.get("primaryUserId").getAsString()); + assertEquals("This user's email is already associated with another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void makingPrimaryUserFailsCauseAlreadyLinkedToAnotherAccountWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser1 = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser1.getSupertokensUserId(), "r1", null, false); + AuthRecipeUserInfo emailPasswordUser2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, emailPasswordUser2.getSupertokensUserId(), emailPasswordUser1.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", emailPasswordUser2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals("r1", response.get("primaryUserId").getAsString()); + assertEquals("This user ID is already linked to another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void createPrimaryUserInTenantWithAnotherStorage() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject coreConfig = new JsonObject(); + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 2); + + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, null, "t1"); + Multitenancy.addNewOrUpdateAppOrTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + coreConfig + ) + ); + + AuthRecipeUserInfo user = EmailPassword.signUp( + tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, process.main)), + process.getProcess(), "test@example.com", "abcd1234"); + + JsonObject userObj; + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + + // check user object + JsonObject jsonUser = response.get("user").getAsJsonObject(); + assert (jsonUser.get("id").getAsString().equals(user.getSupertokensUserId())); + assert (jsonUser.get("tenantIds").getAsJsonArray().size() == 1); + assert (jsonUser.get("tenantIds").getAsJsonArray().get(0).getAsString().equals("t1")); + assert (jsonUser.get("timeJoined").getAsLong() == user.timeJoined); + assert (jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 1); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("test@example.com")); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 0); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 1); + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assert (lM.get("tenantIds").getAsJsonArray().size() == 1); + assert (lM.get("tenantIds").getAsJsonArray().get(0).getAsString().equals("t1")); + assertEquals(lM.get("timeJoined").getAsLong(), user.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), user.getSupertokensUserId()); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "test@example.com"); + assert (lM.entrySet().size() == 6); + userObj = jsonUser; + } + + AuthRecipe.createPrimaryUser(process.main, + tenantIdentifier.toAppIdentifier().withStorage(StorageLayer.getStorage(tenantIdentifier, process.main)), + user.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + assertEquals(response.get("user"), userObj); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void createReturnsFailsWithoutFeatureEnabled() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + JsonObject userObj; + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assertEquals(402, e.statusCode); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/api/GetUserByAccountInfoTest.java b/src/test/java/io/supertokens/test/accountlinking/api/GetUserByAccountInfoTest.java new file mode 100644 index 000000000..b799ecbcf --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/api/GetUserByAccountInfoTest.java @@ -0,0 +1,481 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking.api; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.*; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; +import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.webserver.WebserverAPI; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class GetUserByAccountInfoTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password) + throws DuplicateEmailException, StorageQueryException { + return EmailPassword.signUp(main, email, password); + } + + AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email) + throws EmailChangeNotAllowedException, StorageQueryException { + return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithPhone(Main main, String phone) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, null, phone, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + private JsonObject getUserById(Main main, String userId) throws Exception { + Map params = new HashMap<>(); + params.put("userId", userId); + JsonObject response = HttpRequestForTesting.sendGETRequest(main, "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + return response.get("user").getAsJsonObject(); + } + + private JsonArray getUsersByAccountInfo(Main main, boolean doUnionOfAccountInfo, String email, String phoneNumber, String thirdPartyId, String thirdPartyUserId) throws Exception { + Map params = new HashMap<>(); + params.put("doUnionOfAccountInfo", String.valueOf(doUnionOfAccountInfo)); + if (email != null) { + params.put("email", email); + } + if (phoneNumber != null) { + params.put("phoneNumber", phoneNumber); + } + if (thirdPartyId != null) { + params.put("thirdPartyId", thirdPartyId); + } + if (thirdPartyUserId != null) { + params.put("thirdPartyUserId", thirdPartyUserId); + } + JsonObject response = HttpRequestForTesting.sendGETRequest(main, "", + "http://localhost:3567/users/by-accountinfo", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + return response.get("users").getAsJsonArray(); + } + + @Test + public void testListUsersByAccountInfoForUnlinkedAccounts() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test2@example.com"); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test3@example.com"); + AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + + JsonObject user1json = getUserById(process.getProcess(), user1.getSupertokensUserId()); + JsonObject user2json = getUserById(process.getProcess(), user2.getSupertokensUserId()); + JsonObject user3json = getUserById(process.getProcess(), user3.getSupertokensUserId()); + JsonObject user4json = getUserById(process.getProcess(), user4.getSupertokensUserId()); + + // test for result + assertEquals(user1json, getUsersByAccountInfo(process.getProcess(), false, "test1@example.com", null, null, null).get(0)); + assertEquals(user2json, getUsersByAccountInfo(process.getProcess(), false, null, null, "google", "userid1").get(0)); + assertEquals(user2json, getUsersByAccountInfo(process.getProcess(), false, "test2@example.com", null, "google", "userid1").get(0)); + assertEquals(user3json, getUsersByAccountInfo(process.getProcess(), false, "test3@example.com", null, null, null).get(0)); + assertEquals(user4json, getUsersByAccountInfo(process.getProcess(), false, null, "+919876543210", null, null).get(0)); + + // test for no result + assertEquals(0, getUsersByAccountInfo(process.getProcess(), false, "test1@example.com", "+919876543210", null, null).size()); + assertEquals(0, getUsersByAccountInfo(process.getProcess(), false, "test2@example.com", "+919876543210", null, null).size()); + assertEquals(0, getUsersByAccountInfo(process.getProcess(), false, "test3@example.com", "+919876543210", null, null).size()); + assertEquals(0, getUsersByAccountInfo(process.getProcess(), false, null, "+919876543210", "google", "userid1").size()); + assertEquals(0, getUsersByAccountInfo(process.getProcess(), false, "test1@gmail.com", null, "google", "userid1").size()); + assertEquals(0, getUsersByAccountInfo(process.getProcess(), false, "test3@gmail.com", null, "google", "userid1").size()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUsersByAccountInfoForUnlinkedAccountsWithUnionOption() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test2@example.com"); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test3@example.com"); + AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + + JsonObject user1json = getUserById(process.getProcess(), user1.getSupertokensUserId()); + JsonObject user2json = getUserById(process.getProcess(), user2.getSupertokensUserId()); + JsonObject user3json = getUserById(process.getProcess(), user3.getSupertokensUserId()); + JsonObject user4json = getUserById(process.getProcess(), user4.getSupertokensUserId()); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(StorageLayer.getBaseStorage(process.getProcess())); + { + JsonArray users = getUsersByAccountInfo(process.getProcess(), true, "test1@example.com", "+919876543210", null, null); + assertEquals(2, users.size()); + users.contains(user1json); + users.contains(user4json); + } + { + JsonArray users = getUsersByAccountInfo(process.getProcess(), true, "test1@example.com", null, "google", "userid1"); + assertEquals(2, users.size()); + users.contains(user1json); + users.contains(user2json); + } + { + JsonArray users = getUsersByAccountInfo(process.getProcess(), true, null, "+919876543210", "google", "userid1"); + assertEquals(2, users.size()); + users.contains(user4json); + users.contains(user2json); + } + { + JsonArray users = getUsersByAccountInfo(process.getProcess(), true, "test1@example.com", "+919876543210", "google", "userid1"); + assertEquals(3, users.size()); + users.contains(user1json); + users.contains(user2json); + users.contains(user4json); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUnknownAccountInfo() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage(StorageLayer.getBaseStorage(process.getProcess())); + assertEquals(0, getUsersByAccountInfo(process.getProcess(), false, "test1@example.com", null, null, null).size()); + assertEquals(0, getUsersByAccountInfo(process.getProcess(), false, null, null, "google", "userid1").size()); + assertEquals(0, getUsersByAccountInfo(process.getProcess(), false, "test3@example.com", null, null, null).size()); + assertEquals(0, getUsersByAccountInfo(process.getProcess(), false, null, "+919876543210", null, null).size()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUserByAccountInfoWhenAccountsAreLinked1() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = ThirdParty.signInUp(process.getProcess(), "google", "userid1", "test2@example.com").user; + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage( + StorageLayer.getBaseStorage(process.getProcess())); + + JsonObject primaryUserJson = getUserById(process.getProcess(), user1.getSupertokensUserId()); + + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test1@example.com", null, null, null).get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test2@example.com", null, null, null).get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + null, null, "google", "userid1").get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test1@example.com", null, "google", "userid1").get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test2@example.com", null, "google", "userid1").get(0)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUserByAccountInfoWhenAccountsAreLinked2() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password2"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + JsonObject primaryUserJson = getUserById(process.getProcess(), user1.getSupertokensUserId()); + + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test1@example.com", null, null, null).get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test2@example.com", null, null, null).get(0)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUserByAccountInfoWhenAccountsAreLinked3() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createPasswordlessUserWithEmail(process.getProcess(), "test2@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage( + StorageLayer.getBaseStorage(process.getProcess())); + + JsonObject primaryUserJson = getUserById(process.getProcess(), user1.getSupertokensUserId()); + + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test1@example.com", null, null, null).get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test2@example.com", null, null, null).get(0)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUserByAccountInfoWhenAccountsAreLinked4() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage( + StorageLayer.getBaseStorage(process.getProcess())); + + JsonObject primaryUserJson = getUserById(process.getProcess(), user1.getSupertokensUserId()); + + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test1@example.com", null, null, null).get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + null, "+919876543210", null, null).get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test1@example.com", "+919876543210", null, null).get(0)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testListUserByAccountInfoWhenAccountsAreLinked5() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test2@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = TenantIdentifier.BASE_TENANT.withStorage( + StorageLayer.getBaseStorage(process.getProcess())); + + JsonObject primaryUserJson = getUserById(process.getProcess(), user1.getSupertokensUserId()); + + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test1@example.com", null, null, null).get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test2@example.com", null, null, null).get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + null, null, "google", "userid1").get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test1@example.com", null, "google", "userid1").get(0)); + assertEquals(primaryUserJson, getUsersByAccountInfo(process.getProcess(), false, + "test2@example.com", null, "google", "userid1").get(0)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + + UserIdMapping.createUserIdMapping(process.getProcess(), user1.getSupertokensUserId(), "ext1", "", false); + UserIdMapping.createUserIdMapping(process.getProcess(), user2.getSupertokensUserId(), "ext2", "", false); + UserIdMapping.createUserIdMapping(process.getProcess(), user3.getSupertokensUserId(), "ext3", "", false); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user4.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + JsonObject primaryUserInfo = getUsersByAccountInfo(process.getProcess(), false, + "test@example.com", null, null, null).get(0).getAsJsonObject(); + assertEquals("ext1", primaryUserInfo.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject().get("recipeUserId").getAsString()); + assertEquals("ext2", primaryUserInfo.get("loginMethods").getAsJsonArray().get(1).getAsJsonObject().get("recipeUserId").getAsString()); + assertEquals("ext3", primaryUserInfo.get("loginMethods").getAsJsonArray().get(2).getAsJsonObject().get("recipeUserId").getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/api/GetUserByIdTest.java b/src/test/java/io/supertokens/test/accountlinking/api/GetUserByIdTest.java new file mode 100644 index 000000000..dc6345086 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/api/GetUserByIdTest.java @@ -0,0 +1,544 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking.api; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.*; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.utils.SemVer; +import io.supertokens.webserver.WebserverAPI; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.*; + +public class GetUserByIdTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password) + throws DuplicateEmailException, StorageQueryException { + return EmailPassword.signUp(main, email, password); + } + + AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email) + throws EmailChangeNotAllowedException, StorageQueryException { + return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithPhone(Main main, String phone) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, null, phone, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + @Test + public void testJsonStructure() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user4.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + for (String userId : new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId(), user3.getSupertokensUserId(), user4.getSupertokensUserId()}) { + Map params = new HashMap<>(); + params.put("userId", userId); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + JsonObject user = response.get("user").getAsJsonObject(); + assertEquals(user.entrySet().size(), 8); + assertTrue(user.get("isPrimaryUser").getAsBoolean()); + assertEquals(1, user.get("tenantIds").getAsJsonArray().size()); + assertEquals("public", user.get("tenantIds").getAsJsonArray().get(0).getAsString()); + assertEquals(1, user.get("thirdParty").getAsJsonArray().size()); + assertEquals(4, user.get("loginMethods").getAsJsonArray().size()); + for (JsonElement loginMethodElem : user.get("loginMethods").getAsJsonArray()) { + JsonObject loginMethod = loginMethodElem.getAsJsonObject(); + if (loginMethod.get("recipeId").getAsString().equals("thirdparty")) { + assertEquals(7, loginMethod.entrySet().size()); + assertTrue(loginMethod.has("thirdParty")); + assertEquals(2, loginMethod.get("thirdParty").getAsJsonObject().entrySet().size()); + } else { + assertEquals(6, loginMethod.entrySet().size()); + } + if (loginMethod.has("email")) { + assertEquals("test@example.com", loginMethod.get("email").getAsString()); + } else if (loginMethod.has("phoneNumber")) { + assertEquals("+919876543210", loginMethod.get("phoneNumber").getAsString()); + assertTrue(loginMethod.get("verified").getAsBoolean()); + } else { + fail(); + } + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatEmailIsAUnionOfLinkedAccounts1() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test2@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test3@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + for (String userId : new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId(), user3.getSupertokensUserId()}) { + Map params = new HashMap<>(); + params.put("userId", userId); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + JsonObject user = response.get("user").getAsJsonObject(); + + Set emails = new HashSet<>(); + for (JsonElement emailElem : user.get("emails").getAsJsonArray()) { + emails.add(emailElem.getAsString()); + } + assertEquals(3, emails.size()); + assertTrue(emails.contains("test1@example.com")); + assertTrue(emails.contains("test2@example.com")); + assertTrue(emails.contains("test3@example.com")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatEmailIsAUnionOfLinkedAccounts2() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test2@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + for (String userId : new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId()}) { + Map params = new HashMap<>(); + params.put("userId", userId); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + JsonObject user = response.get("user").getAsJsonObject(); + + Set emails = new HashSet<>(); + for (JsonElement emailElem : user.get("emails").getAsJsonArray()) { + emails.add(emailElem.getAsString()); + } + assertEquals(2, emails.size()); + assertTrue(emails.contains("test1@example.com")); + assertTrue(emails.contains("test2@example.com")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatEmailIsAUnionOfLinkedAccounts3() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createPasswordlessUserWithEmail(process.getProcess(), "test2@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + for (String userId : new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId()}) { + Map params = new HashMap<>(); + params.put("userId", userId); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + JsonObject user = response.get("user").getAsJsonObject(); + + Set emails = new HashSet<>(); + for (JsonElement emailElem : user.get("emails").getAsJsonArray()) { + emails.add(emailElem.getAsString()); + } + assertEquals(2, emails.size()); + assertTrue(emails.contains("test1@example.com")); + assertTrue(emails.contains("test2@example.com")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatEmailIsAUnionOfLinkedAccounts4() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createThirdPartyUser(process.getProcess(), "google", "googleid", "test1@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createPasswordlessUserWithEmail(process.getProcess(), "test2@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + for (String userId : new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId()}) { + Map params = new HashMap<>(); + params.put("userId", userId); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + JsonObject user = response.get("user").getAsJsonObject(); + + Set emails = new HashSet<>(); + for (JsonElement emailElem : user.get("emails").getAsJsonArray()) { + emails.add(emailElem.getAsString()); + } + assertEquals(2, emails.size()); + assertTrue(emails.contains("test1@example.com")); + assertTrue(emails.contains("test2@example.com")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatPhoneNumberIsUnionOfLinkedAccounts1() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createPasswordlessUserWithPhone(process.getProcess(), "+911234567890"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + for (String userId : new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId()}) { + Map params = new HashMap<>(); + params.put("userId", userId); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + JsonObject user = response.get("user").getAsJsonObject(); + + Set phoneNumbers = new HashSet<>(); + for (JsonElement phoneNumberElem : user.get("phoneNumbers").getAsJsonArray()) { + phoneNumbers.add(phoneNumberElem.getAsString()); + } + assertEquals(2, phoneNumbers.size()); + assertTrue(phoneNumbers.contains("+919876543210")); + assertTrue(phoneNumbers.contains("+911234567890")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatPhoneNumberIsUnionOfLinkedAccounts2() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createPasswordlessUserWithPhone(process.getProcess(), "+911234567890"); + Thread.sleep(50); + AuthRecipeUserInfo user3 = createThirdPartyUser(process.getProcess(), "google", "googleid", "test@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + for (String userId : new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId()}) { + Map params = new HashMap<>(); + params.put("userId", userId); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + JsonObject user = response.get("user").getAsJsonObject(); + + Set phoneNumbers = new HashSet<>(); + for (JsonElement phoneNumberElem : user.get("phoneNumbers").getAsJsonArray()) { + phoneNumbers.add(phoneNumberElem.getAsString()); + } + assertEquals(2, phoneNumbers.size()); + assertTrue(phoneNumbers.contains("+919876543210")); + assertTrue(phoneNumbers.contains("+911234567890")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThatPhoneNumberIsUnionOfLinkedAccounts3() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createPasswordlessUserWithPhone(process.getProcess(), "+911234567890"); + Thread.sleep(50); + AuthRecipeUserInfo user3 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + for (String userId : new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId()}) { + Map params = new HashMap<>(); + params.put("userId", userId); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + JsonObject user = response.get("user").getAsJsonObject(); + + Set phoneNumbers = new HashSet<>(); + for (JsonElement phoneNumberElem : user.get("phoneNumbers").getAsJsonArray()) { + phoneNumbers.add(phoneNumberElem.getAsString()); + } + assertEquals(2, phoneNumbers.size()); + assertTrue(phoneNumbers.contains("+919876543210")); + assertTrue(phoneNumbers.contains("+911234567890")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password1"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "userid1", "test@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test@example.com"); + Thread.sleep(50); + AuthRecipeUserInfo user4 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543210"); + + UserIdMapping.createUserIdMapping(process.getProcess(), user1.getSupertokensUserId(), "ext1", "", false); + UserIdMapping.createUserIdMapping(process.getProcess(), user2.getSupertokensUserId(), "ext2", "", false); + UserIdMapping.createUserIdMapping(process.getProcess(), user3.getSupertokensUserId(), "ext3", "", false); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user4.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + for (String userId : new String[]{user1.getSupertokensUserId(), user2.getSupertokensUserId(), user3.getSupertokensUserId(), user4.getSupertokensUserId(), "ext1", "ext2", "ext3"}) { + Map params = new HashMap<>(); + params.put("userId", userId); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + SemVer.v4_0.get(), ""); + JsonObject user = response.get("user").getAsJsonObject(); + assertEquals("ext1", user.get("id").getAsString()); + assertEquals("ext1", user.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject().get("recipeUserId").getAsString()); + assertEquals("ext2", user.get("loginMethods").getAsJsonArray().get(1).getAsJsonObject().get("recipeUserId").getAsString()); + assertEquals("ext3", user.get("loginMethods").getAsJsonArray().get(2).getAsJsonObject().get("recipeUserId").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUnknownUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + Map params = new HashMap<>(); + params.put("userId", "unknownid"); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals("UNKNOWN_USER_ID_ERROR", response.get("status").getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/api/LinkAccountsAPITest.java b/src/test/java/io/supertokens/test/accountlinking/api/LinkAccountsAPITest.java new file mode 100644 index 000000000..1adae1ba1 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/api/LinkAccountsAPITest.java @@ -0,0 +1,639 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.httpRequest.HttpResponseException; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.utils.SemVer; +import io.supertokens.webserver.WebserverAPI; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class LinkAccountsAPITest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void linkReturnsTrue() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "abcd1234"); + + AuthRecipe.createPrimaryUser(process.main, user2.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + params.addProperty("primaryUserId", user2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("accountsAlreadyLinked").getAsBoolean()); + assertTrue(response.has("user")); + } + + AuthRecipe.linkAccounts(process.main, user.getSupertokensUserId(), user2.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + params.addProperty("primaryUserId", user2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("accountsAlreadyLinked").getAsBoolean()); + assertTrue(response.has("user")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void canLinkReturnsTrueWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + UserIdMapping.createUserIdMapping(process.main, user.getSupertokensUserId(), "r1", null, false); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "abcd1234"); + UserIdMapping.createUserIdMapping(process.main, user2.getSupertokensUserId(), "r2", null, false); + + AuthRecipe.createPrimaryUser(process.main, user2.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", "r1"); + params.addProperty("primaryUserId", "r2"); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("accountsAlreadyLinked").getAsBoolean()); + assertTrue(response.has("user")); + } + + AuthRecipe.linkAccounts(process.main, user.getSupertokensUserId(), user2.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", "r1"); + params.addProperty("primaryUserId", "r2"); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("accountsAlreadyLinked").getAsBoolean()); + assertTrue(response.has("user")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void canLinkUserBadInput() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + JsonObject params = new JsonObject(); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Field name 'recipeUserId' is invalid in JSON " + + "input")); + } + } + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", "random"); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Field name 'primaryUserId' is invalid in JSON" + + " input")); + } + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "abcd1234"); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user2.getSupertokensUserId()); + params.addProperty("primaryUserId", "random"); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Unknown user ID provided")); + } + } + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", "random"); + params.addProperty("primaryUserId", user.getSupertokensUserId()); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Unknown user ID provided")); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkingUsersFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test2@example.com"); + + AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId()); + + ThirdParty.SignInUpResponse signInUpResponse2 = ThirdParty.signInUp(process.main, "fb", "user-fb", + "test@example.com"); + + + { + JsonObject params = new JsonObject(); + params.addProperty("primaryUserId", signInUpResponse.user.getSupertokensUserId()); + params.addProperty("recipeUserId", signInUpResponse2.user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals(emailPasswordUser.getSupertokensUserId(), response.get("primaryUserId").getAsString()); + assertEquals("This user's email is already associated with another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkingUsersFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryUserWithUserIdMapping() + throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser.getSupertokensUserId(), "e1", null, false); + + AuthRecipe.CreatePrimaryUserResult result = AuthRecipe.createPrimaryUser(process.main, emailPasswordUser.getSupertokensUserId()); + assert (!result.wasAlreadyAPrimaryUser); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, "google", "user-google", + "test2@example.com"); + UserIdMapping.createUserIdMapping(process.main, signInUpResponse.user.getSupertokensUserId(), "e2", null, false); + + AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId()); + + ThirdParty.SignInUpResponse signInUpResponse2 = ThirdParty.signInUp(process.main, "fb", "user-fb", + "test@example.com"); + UserIdMapping.createUserIdMapping(process.main, signInUpResponse2.user.getSupertokensUserId(), "e3", null, false); + + + { + JsonObject params = new JsonObject(); + params.addProperty("primaryUserId", "e2"); + params.addProperty("recipeUserId", "e3"); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals("e1", response.get("primaryUserId").getAsString()); + assertEquals("This user's email is already associated with another user ID", + response.get("description").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkingUserFailsCauseAlreadyLinkedToAnotherAccount() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser1 = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + AuthRecipeUserInfo emailPasswordUser2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, emailPasswordUser2.getSupertokensUserId(), emailPasswordUser1.getSupertokensUserId()); + + AuthRecipeUserInfo emailPasswordUser3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser3.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", emailPasswordUser2.getSupertokensUserId()); + params.addProperty("primaryUserId", emailPasswordUser3.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(4, response.entrySet().size()); + assertEquals("RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals(emailPasswordUser1.getSupertokensUserId(), response.get("primaryUserId").getAsString()); + assertEquals("The input recipe user ID is already linked to another user ID", + response.get("description").getAsString()); + assertTrue(response.has("user")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + + @Test + public void makingPrimaryUserFailsCauseAlreadyLinkedToAnotherAccountWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser1 = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser1.getSupertokensUserId(), "r1", null, false); + AuthRecipeUserInfo emailPasswordUser2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser2.getSupertokensUserId(), "r2", null, false); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, emailPasswordUser2.getSupertokensUserId(), emailPasswordUser1.getSupertokensUserId()); + + AuthRecipeUserInfo emailPasswordUser3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", + "pass1234"); + UserIdMapping.createUserIdMapping(process.main, emailPasswordUser3.getSupertokensUserId(), "r3", null, false); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser3.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", "r2"); + params.addProperty("primaryUserId", "r3"); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(4, response.entrySet().size()); + assertEquals("RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals("r1", response.get("primaryUserId").getAsString()); + assertEquals("The input recipe user ID is already linked to another user ID", + response.get("description").getAsString()); + assertTrue(response.has("user")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void inputUserIsNotAPrimaryUserTest() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "abcd1234"); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + params.addProperty("primaryUserId", user2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(1, response.entrySet().size()); + assertEquals("INPUT_USER_IS_NOT_A_PRIMARY_USER", response.get("status").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkReturnsFailsWithoutFeatureEnabled() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "abcd1234"); + + JsonObject userObj; + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + params.addProperty("primaryUserId", user2.getSupertokensUserId()); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assertEquals(402, e.statusCode); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + } + + @Test + public void testUserObjectInLinkAccountsResponse() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "abcd1234"); + + AuthRecipe.createPrimaryUser(process.main, user2.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + params.addProperty("primaryUserId", user2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("accountsAlreadyLinked").getAsBoolean()); + JsonObject userObj = response.get("user").getAsJsonObject(); + + Map getUserParams = new HashMap<>(); + getUserParams.put("userId", user.getSupertokensUserId()); + JsonObject getUserResponse = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", getUserParams, 1000, 1000, null, + SemVer.v4_0.get(), ""); + JsonObject userObj2 = response.get("user").getAsJsonObject(); + assertEquals(userObj, userObj2); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkingUserFailsCauseAlreadyLinkedToAnotherAccountReturnsUserObject() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo emailPasswordUser1 = EmailPassword.signUp(process.getProcess(), "test@example.com", + "pass1234"); + AuthRecipeUserInfo emailPasswordUser2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, emailPasswordUser2.getSupertokensUserId(), emailPasswordUser1.getSupertokensUserId()); + + AuthRecipeUserInfo emailPasswordUser3 = EmailPassword.signUp(process.getProcess(), "test3@example.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, emailPasswordUser3.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", emailPasswordUser2.getSupertokensUserId()); + params.addProperty("primaryUserId", emailPasswordUser3.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/link", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(4, response.entrySet().size()); + assertEquals("RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + response.get("status").getAsString()); + assertEquals(emailPasswordUser1.getSupertokensUserId(), response.get("primaryUserId").getAsString()); + assertEquals("The input recipe user ID is already linked to another user ID", + response.get("description").getAsString()); + + JsonObject userObj = response.get("user").getAsJsonObject(); + + Map getUserParams = new HashMap<>(); + getUserParams.put("userId", emailPasswordUser1.getSupertokensUserId()); + JsonObject getUserResponse = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", getUserParams, 1000, 1000, null, + SemVer.v4_0.get(), ""); + JsonObject userObj2 = response.get("user").getAsJsonObject(); + assertEquals(userObj, userObj2); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + +} diff --git a/src/test/java/io/supertokens/test/accountlinking/api/TestRecipeUserIdInSignInUpAPIs.java b/src/test/java/io/supertokens/test/accountlinking/api/TestRecipeUserIdInSignInUpAPIs.java new file mode 100644 index 000000000..b09b69976 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/api/TestRecipeUserIdInSignInUpAPIs.java @@ -0,0 +1,770 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking.api; + +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.*; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class TestRecipeUserIdInSignInUpAPIs { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + + AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password) + throws DuplicateEmailException, StorageQueryException { + return EmailPassword.signUp(main, email, password); + } + + AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email) + throws EmailChangeNotAllowedException, StorageQueryException { + return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithPhone(Main main, String phone) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, null, phone, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + @Test + public void testEmailPasswordSignUp() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "random@gmail.com"); + responseBody.addProperty("password", "validPass123"); + + JsonObject signUpResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signup", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signUpResponse.get("status").getAsString(), "OK"); + assertEquals(signUpResponse.entrySet().size(), 3); + assertEquals(signUpResponse.get("recipeUserId"), signUpResponse.get("user").getAsJsonObject().get("id")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testEmailPasswordSignIn() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = createEmailPasswordUser(process.getProcess(), + "test@example.com", "password"); + + { + // Before account linking + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "test@example.com"); + responseBody.addProperty("password", "password"); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + assertEquals(signInResponse.get("recipeUserId").getAsString(), user.getSupertokensUserId()); + } + + AuthRecipeUserInfo user2 = createPasswordlessUserWithEmail(process.getProcess(), + "test@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user2.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + { + // After account linking + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "test@example.com"); + responseBody.addProperty("password", "password"); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + assertEquals(signInResponse.get("recipeUserId").getAsString(), user.getSupertokensUserId()); + } + + // With another email password user + AuthRecipeUserInfo user3 = createEmailPasswordUser(process.getProcess(), + "test2@example.com", "password"); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + { + // After account linking + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "test@example.com"); + responseBody.addProperty("password", "password"); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + assertEquals(signInResponse.get("recipeUserId").getAsString(), user.getSupertokensUserId()); + } + { + // After account linking + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "test2@example.com"); + responseBody.addProperty("password", "password"); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + assertEquals(signInResponse.get("recipeUserId").getAsString(), user3.getSupertokensUserId()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testThirdPartySignInUp() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String userId = null; + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + assertEquals(4, response.entrySet().size()); + assertEquals(response.get("recipeUserId"), response.get("user").getAsJsonObject().get("id")); + userId = response.get("recipeUserId").getAsString(); + } + + { + // Without account linking + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + + AuthRecipeUserInfo user2 = createEmailPasswordUser(process.getProcess(), + "test@example.com", "password"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user2.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), userId, primaryUser.getSupertokensUserId()); + + { + // After account linking + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + + AuthRecipeUserInfo user3 = createThirdPartyUser(process.getProcess(), "facebook", "fb-user", "test@example.com"); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + { + // After account linking + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + { + // After account linking + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "facebook"); + signUpRequestBody.addProperty("thirdPartyUserId", "fb-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + assertEquals(4, response.entrySet().size()); + assertEquals(user3.getSupertokensUserId(), response.get("recipeUserId").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testPasswordlessConsumeCode() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String userId = null; + { + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), "test@example.com", null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(response.get("recipeUserId"), response.get("user").getAsJsonObject().get("id")); + userId = response.get("recipeUserId").getAsString(); + } + + { // Without account linking + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), "test@example.com", null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "google-user", "test@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user2.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), userId, primaryUser.getSupertokensUserId()); + + { // after account linking + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), "test@example.com", null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test2@example.com"); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + { // after account linking + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), "test@example.com", null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + { // after account linking + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), "test2@example.com", null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(user3.getSupertokensUserId(), response.get("recipeUserId").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testPasswordlessConsumeCodeForPhone() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String userId = null; + { + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), null, "+919876543210", null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(response.get("recipeUserId"), response.get("user").getAsJsonObject().get("id")); + userId = response.get("recipeUserId").getAsString(); + } + + { // Without account linking + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), null, "+919876543210", null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "google-user", "test@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user2.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), userId, primaryUser.getSupertokensUserId()); + + { // after account linking + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), null, "+919876543210", null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + + AuthRecipeUserInfo user3 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543211"); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + { // after account linking + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), null, "+919876543210", null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + { // after account linking + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), null, "+919876543211", null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(user3.getSupertokensUserId(), response.get("recipeUserId").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testPasswordlessConsumeCodeForPhoneAndEmail() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String userId = null; + { + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), null, "+919876543210", null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(response.get("recipeUserId"), response.get("user").getAsJsonObject().get("id")); + userId = response.get("recipeUserId").getAsString(); + } + + Passwordless.updateUser(process.getProcess(), userId, new Passwordless.FieldUpdate("test@example.com"), null); + + { // Without account linking - phone + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), null, "+919876543210", null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + { // Without account linking - email + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), "test@example.com", null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "google-user", "test@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user2.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), userId, primaryUser.getSupertokensUserId()); + + { // after account linking - phone + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), null, "+919876543210", null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + { // after account linking - email + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), "test@example.com", null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + + AuthRecipeUserInfo user3 = createPasswordlessUserWithPhone(process.getProcess(), "+919876543211"); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + { // after account linking - phone + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), null, "+919876543210", null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + { // after account linking - email + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), "test@example.com", null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertEquals(4, response.entrySet().size()); + assertEquals(userId, response.get("recipeUserId").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testWithEmailPasswordUserWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = createEmailPasswordUser(process.getProcess(), + "test@example.com", "password"); + UserIdMapping.createUserIdMapping(process.getProcess(), user.getSupertokensUserId(), "extuserid", "", false); + + { + // Before account linking + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "test@example.com"); + responseBody.addProperty("password", "password"); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + assertEquals(signInResponse.get("recipeUserId").getAsString(), "extuserid"); + } + + AuthRecipeUserInfo user2 = createPasswordlessUserWithEmail(process.getProcess(), + "test@example.com"); + + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user2.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + { + // After account linking + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "test@example.com"); + responseBody.addProperty("password", "password"); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + assertEquals(signInResponse.get("recipeUserId").getAsString(), "extuserid"); + } + + // With another email password user + AuthRecipeUserInfo user3 = createEmailPasswordUser(process.getProcess(), + "test2@example.com", "password"); + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + + { + // After account linking + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "test@example.com"); + responseBody.addProperty("password", "password"); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + assertEquals(signInResponse.get("recipeUserId").getAsString(), "extuserid"); + } + { + // After account linking + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "test2@example.com"); + responseBody.addProperty("password", "password"); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + assertEquals(signInResponse.get("recipeUserId").getAsString(), user3.getSupertokensUserId()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/api/UnlinkAccountsAPITest.java b/src/test/java/io/supertokens/test/accountlinking/api/UnlinkAccountsAPITest.java new file mode 100644 index 000000000..b0ae61018 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/api/UnlinkAccountsAPITest.java @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.session.Session; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.httpRequest.HttpResponseException; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.webserver.WebserverAPI; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + + +public class UnlinkAccountsAPITest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void unlinkAccountSuccess() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()); + + Session.createNewSession(process.main, user2.getSupertokensUserId(), new JsonObject(), new JsonObject()); + String[] sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId()); + assert (sessions.length == 1); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user2.getSupertokensUserId()); + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/unlink", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("wasRecipeUserDeleted").getAsBoolean()); + assertTrue(response.get("wasLinked").getAsBoolean()); + } + + + AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId()); + assert (!refetchUser2.isPrimaryUser); + assert (refetchUser2.getSupertokensUserId().equals(user2.getSupertokensUserId())); + assert (refetchUser2.loginMethods.length == 1); + assert (refetchUser2.loginMethods[0].getSupertokensUserId().equals(user2.getSupertokensUserId())); + + AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId()); + assert (!refetchUser2.equals(refetchUser)); + assert (refetchUser.isPrimaryUser); + assert (refetchUser.loginMethods.length == 1); + assert (refetchUser.loginMethods[0].getSupertokensUserId().equals(user.getSupertokensUserId())); + + // cause linkAccounts revokes sessions for the recipe user ID + sessions = Session.getAllNonExpiredSessionHandlesForUser(process.main, user2.getSupertokensUserId()); + assert (sessions.length == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + + @Test + public void unlinkAccountBadRequest() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + try { + JsonObject params = new JsonObject(); + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/unlink", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage() + .equals("Http error. Status Code: 400. Message: Field name 'recipeUserId' is invalid in JSON " + + "input")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void unlinkAccountSuccessWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + assert (!user2.isPrimaryUser); + UserIdMapping.createUserIdMapping(process.main, user2.getSupertokensUserId(), "e2", null, false); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", "e2"); + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/unlink", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("wasRecipeUserDeleted").getAsBoolean()); + assertTrue(response.get("wasLinked").getAsBoolean()); + } + + + AuthRecipeUserInfo refetchUser2 = AuthRecipe.getUserById(process.main, user2.getSupertokensUserId()); + assert (!refetchUser2.isPrimaryUser); + assert (refetchUser2.getSupertokensUserId().equals(user2.getSupertokensUserId())); + assert (refetchUser2.loginMethods.length == 1); + assert (refetchUser2.loginMethods[0].getSupertokensUserId().equals(user2.getSupertokensUserId())); + + AuthRecipeUserInfo refetchUser = AuthRecipe.getUserById(process.main, user.getSupertokensUserId()); + assert (!refetchUser2.equals(refetchUser)); + assert (refetchUser.isPrimaryUser); + assert (refetchUser.loginMethods.length == 1); + assert (refetchUser.loginMethods[0].getSupertokensUserId().equals(user.getSupertokensUserId())); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void unlinkAccountWithoutPrimaryUserId() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/unlink", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("wasRecipeUserDeleted").getAsBoolean()); + assertFalse(response.get("wasLinked").getAsBoolean()); + } + + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void unlinkAccountWithUnknownUserId() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + try { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", "random"); + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/unlink", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assert (e.statusCode == 400); + assert (e.getMessage().equals("Http error. Status Code: 400. Message: Unknown user ID provided")); + } + + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void unlinkAccountSuccessButDeletesUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/unlink", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("wasRecipeUserDeleted").getAsBoolean()); + assertTrue(response.get("wasLinked").getAsBoolean()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/accountlinking/api/UserPaginationTest.java b/src/test/java/io/supertokens/test/accountlinking/api/UserPaginationTest.java new file mode 100644 index 000000000..99cf76376 --- /dev/null +++ b/src/test/java/io/supertokens/test/accountlinking/api/UserPaginationTest.java @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.accountlinking.api; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.*; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class UserPaginationTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + + AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password) + throws DuplicateEmailException, StorageQueryException { + return EmailPassword.signUp(main, email, password); + } + + AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email) + throws EmailChangeNotAllowedException, StorageQueryException { + return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + AuthRecipeUserInfo createPasswordlessUserWithPhone(Main main, String phone) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, null, phone, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + private JsonObject getUsers(Main main) throws Exception { + Map params = new HashMap<>(); + return HttpRequestForTesting.sendGETRequest(main, "", + "http://localhost:3567/users", params, 1000, 1000, null, + SemVer.v4_0.get(), ""); + } + + private JsonArray getUsersFromAllPages(Main main, int pageSize, String[] recipeFilters) throws Exception { + Map params = new HashMap<>(); + + if (recipeFilters != null) { + params.put("includeRecipeIds", String.join(",", recipeFilters)); + } + + params.put("limit", String.valueOf(pageSize)); + + JsonArray result = new JsonArray(); + + JsonObject response = HttpRequestForTesting.sendGETRequest(main, "", + "http://localhost:3567/users", params, 1000, 1000, null, + SemVer.v4_0.get(), ""); + + result.addAll(response.get("users").getAsJsonArray()); + while (response.get("nextPaginationToken") != null) { + String paginationToken = response.get("nextPaginationToken").getAsString(); + params.put("paginationToken", paginationToken); + + response = HttpRequestForTesting.sendGETRequest(main, "", + "http://localhost:3567/users", params, 1000, 1000, null, + SemVer.v4_0.get(), ""); + result.addAll(response.get("users").getAsJsonArray()); + } + + return result; + } + + @Test + public void testUserPaginationResultJson() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user2 = createEmailPasswordUser(process.getProcess(), "test2@example.com", "password"); + AuthRecipeUserInfo user3 = createPasswordlessUserWithEmail(process.getProcess(), "test3@example.com"); + AuthRecipeUserInfo user4 = createPasswordlessUserWithEmail(process.getProcess(), "test4@example.com"); + AuthRecipeUserInfo user5 = createPasswordlessUserWithPhone(process.getProcess(), "+1234567890"); + AuthRecipeUserInfo user6 = createPasswordlessUserWithPhone(process.getProcess(), "+1234567891"); + AuthRecipeUserInfo user7 = createThirdPartyUser(process.getProcess(), "google", "test7", "test7@example.com"); + AuthRecipeUserInfo user8 = createThirdPartyUser(process.getProcess(), "google", "test8", "test8@example.com"); + + { + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user3.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + } + + { + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user2.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user5.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user7.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + } + + { + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), user6.getSupertokensUserId()).user; + AuthRecipe.linkAccounts(process.getProcess(), user8.getSupertokensUserId(), primaryUser.getSupertokensUserId()); + } + + + Map params = new HashMap<>(); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/users", params, 1000, 1000, null, + SemVer.v4_0.get(), ""); + + JsonArray users = response.get("users").getAsJsonArray(); + assertEquals(4,users.size()); + + { + params = new HashMap<>(); + params.put("userId", user1.getSupertokensUserId()); + JsonObject userResponse = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + SemVer.v4_0.get(), ""); + userResponse.remove("status"); + assertEquals(userResponse.get("user"), users.get(0)); + } + + { + params = new HashMap<>(); + params.put("userId", user2.getSupertokensUserId()); + JsonObject userResponse = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + SemVer.v4_0.get(), ""); + userResponse.remove("status"); + assertEquals(userResponse.get("user"), users.get(1)); + } + + { + params = new HashMap<>(); + params.put("userId", user4.getSupertokensUserId()); + JsonObject userResponse = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + SemVer.v4_0.get(), ""); + userResponse.remove("status"); + assertEquals(userResponse.get("user"), users.get(2)); + } + + { + params = new HashMap<>(); + params.put("userId", user6.getSupertokensUserId()); + JsonObject userResponse = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + SemVer.v4_0.get(), ""); + userResponse.remove("status"); + assertEquals(userResponse.get("user"), users.get(3)); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUserPaginationWithManyUsers() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + Map userInfoMap = new HashMap<>(); + Set userIds = new HashSet<>(); + Set emailPasswordUsers = new HashSet<>(); + Set passwordlessUsers = new HashSet<>(); + Set thirdPartyUsers = new HashSet<>(); + + // emailpassword users + for (int i=0; i < 200; i++) { + AuthRecipeUserInfo user = createEmailPasswordUser(process.getProcess(), "epuser" + i + "@gmail.com", "password" + i); + userInfoMap.put(user.getSupertokensUserId(), user); + userIds.add(user.getSupertokensUserId()); + emailPasswordUsers.add(user.getSupertokensUserId()); + Thread.sleep(10); + } + + // passwordless users with email + for (int i=0; i < 200; i++) { + AuthRecipeUserInfo user = createPasswordlessUserWithEmail(process.getProcess(), "pluser" + i + "@gmail.com"); + userInfoMap.put(user.getSupertokensUserId(), user); + userIds.add(user.getSupertokensUserId()); + passwordlessUsers.add(user.getSupertokensUserId()); + Thread.sleep(10); + } + + // passwordless users with phone + for (int i=0; i < 200; i++) { + AuthRecipeUserInfo user = createPasswordlessUserWithPhone(process.getProcess(), "+1234567890" + i); + userInfoMap.put(user.getSupertokensUserId(), user); + userIds.add(user.getSupertokensUserId()); + passwordlessUsers.add(user.getSupertokensUserId()); + Thread.sleep(10); + } + + // thirdparty users + for (int i=0; i < 200; i++) { + AuthRecipeUserInfo user = createThirdPartyUser(process.getProcess(), "google", "tpuser" + i, "tpuser" + i + "@gmail.com"); + userInfoMap.put(user.getSupertokensUserId(), user); + userIds.add(user.getSupertokensUserId()); + thirdPartyUsers.add(user.getSupertokensUserId()); + Thread.sleep(10); + } + + Map primaryUserIdMap = new HashMap<>(); + List primaryUserIds = new ArrayList<>(); + + // Randomly link accounts + Random rand = new Random(); + while (!userIds.isEmpty()) { + int numAccountsToLink = Math.min(rand.nextInt(3) + 1, userIds.size()); + List userIdsToLink = new ArrayList<>(); + + for (int i=0; i < numAccountsToLink; i++) { + String[] userIdsArray = userIds.toArray(new String[0]); + String userId = userIdsArray[rand.nextInt(userIds.size())]; + userIdsToLink.add(userId); + userIds.remove(userId); + } + + for (String userId : userIdsToLink) { + primaryUserIdMap.put(userId, userIdsToLink.get(0)); + } + + if (userIdsToLink.size() > 1) { + AuthRecipeUserInfo primaryUser = AuthRecipe.createPrimaryUser(process.getProcess(), userIdsToLink.get(0)).user; + primaryUserIds.add(primaryUser.getSupertokensUserId()); + + for (int i=1; i < userIdsToLink.size(); i++) { + AuthRecipe.linkAccounts(process.getProcess(), userIdsToLink.get(i), primaryUser.getSupertokensUserId()); + } + } else { + primaryUserIds.add(userIdsToLink.get(0)); + } + } + + // Pagination tests + { + JsonArray usersResult = getUsersFromAllPages(process.getProcess(), 10, null); + assertEquals(primaryUserIds.size(), usersResult.size()); + } + + // Test pagination with recipe filters + { + JsonArray usersResult = getUsersFromAllPages(process.getProcess(), 20, new String[]{"emailpassword"}); + Set primaryUsers = new HashSet<>(); + for (String userId : emailPasswordUsers) { + primaryUsers.add(primaryUserIdMap.get(userId)); + } + + assertEquals(primaryUsers.size(), usersResult.size()); + } + { + JsonArray usersResult = getUsersFromAllPages(process.getProcess(), 20, new String[]{"passwordless"}); + Set primaryUsers = new HashSet<>(); + for (String userId : passwordlessUsers) { + primaryUsers.add(primaryUserIdMap.get(userId)); + } + + assertEquals(primaryUsers.size(), usersResult.size()); + } + { + JsonArray usersResult = getUsersFromAllPages(process.getProcess(), 20, new String[]{"thirdparty"}); + Set primaryUsers = new HashSet<>(); + for (String userId : thirdPartyUsers) { + primaryUsers.add(primaryUserIdMap.get(userId)); + } + + assertEquals(primaryUsers.size(), usersResult.size()); + } + { + JsonArray usersResult = getUsersFromAllPages(process.getProcess(), 20, new String[]{"emailpassword", "passwordless"}); + Set primaryUsers = new HashSet<>(); + for (String userId : emailPasswordUsers) { + primaryUsers.add(primaryUserIdMap.get(userId)); + } + for (String userId : passwordlessUsers) { + primaryUsers.add(primaryUserIdMap.get(userId)); + } + + assertEquals(primaryUsers.size(), usersResult.size()); + } + { + JsonArray usersResult = getUsersFromAllPages(process.getProcess(), 20, new String[]{"thirdparty", "passwordless"}); + Set primaryUsers = new HashSet<>(); + for (String userId : thirdPartyUsers) { + primaryUsers.add(primaryUserIdMap.get(userId)); + } + for (String userId : passwordlessUsers) { + primaryUsers.add(primaryUserIdMap.get(userId)); + } + + assertEquals(primaryUsers.size(), usersResult.size()); + } + { + JsonArray usersResult = getUsersFromAllPages(process.getProcess(), 20, new String[]{"thirdparty", "passwordless", "emailpassword"}); + assertEquals(primaryUserIds.size(), usersResult.size()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/authRecipe/AuthRecipeStorageTest.java b/src/test/java/io/supertokens/test/authRecipe/AuthRecipeStorageTest.java index 90a0ddc6c..0443dcbbb 100644 --- a/src/test/java/io/supertokens/test/authRecipe/AuthRecipeStorageTest.java +++ b/src/test/java/io/supertokens/test/authRecipe/AuthRecipeStorageTest.java @@ -20,7 +20,7 @@ import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; @@ -64,8 +64,8 @@ public void testIfAUserIdIsASuperTokensUserId() throws Exception { assertFalse(storage.doesUserIdExist(new TenantIdentifier(null, null, null), "unknownUser")); // create a user and check that the userId exists - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - assertTrue(storage.doesUserIdExist(new TenantIdentifier(null, null, null), userInfo.id)); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + assertTrue(storage.doesUserIdExist(new TenantIdentifier(null, null, null), userInfo.getSupertokensUserId())); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/test/authRecipe/DeleteUserAPIWithUserIdMappingTest.java b/src/test/java/io/supertokens/test/authRecipe/DeleteUserAPIWithUserIdMappingTest.java index 99bf07581..aa5d0e538 100644 --- a/src/test/java/io/supertokens/test/authRecipe/DeleteUserAPIWithUserIdMappingTest.java +++ b/src/test/java/io/supertokens/test/authRecipe/DeleteUserAPIWithUserIdMappingTest.java @@ -16,13 +16,11 @@ package io.supertokens.test.authRecipe; -import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; -import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -32,15 +30,12 @@ import io.supertokens.useridmapping.UserIdType; import io.supertokens.usermetadata.UserMetadata; import io.supertokens.utils.SemVer; -import junit.framework.TestCase; import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; -import java.util.ArrayList; - import static org.junit.Assert.*; public class DeleteUserAPIWithUserIdMappingTest { @@ -70,8 +65,8 @@ public void createAUserMapTheirIdCreateMetadataWithExternalIdAndDelete() throws // deleting with superTokensUserId { // create User - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalId = "externalId"; // map their id @@ -92,7 +87,7 @@ public void createAUserMapTheirIdCreateMetadataWithExternalIdAndDelete() throws // check that user doesnt exist { - UserInfo response = EmailPassword.getUserUsingId(process.main, superTokensUserId); + AuthRecipeUserInfo response = EmailPassword.getUserUsingId(process.main, superTokensUserId); assertNull(response); } @@ -127,24 +122,24 @@ public void testDeleteUserBehaviorInIntermediateStateWithUser_1sUserId() throws } // create an EmailPassword User - UserInfo userInfo_1 = EmailPassword.signUp(process.main, "test@example.com", "testPassword123"); + AuthRecipeUserInfo userInfo_1 = EmailPassword.signUp(process.main, "test@example.com", "testPassword123"); // associate some data with user JsonObject data = new JsonObject(); data.addProperty("test", "testData"); - UserMetadata.updateUserMetadata(process.main, userInfo_1.id, data); + UserMetadata.updateUserMetadata(process.main, userInfo_1.getSupertokensUserId(), data); // create a new User who we would like to migrate the EmailPassword user to ThirdParty.SignInUpResponse userInfo_2 = ThirdParty.signInUp(process.main, "google", "test-google", "test123@example.com"); // force create a mapping between the thirdParty user and EmailPassword user - UserIdMapping.createUserIdMapping(process.main, userInfo_2.user.id, userInfo_1.id, null, true); + UserIdMapping.createUserIdMapping(process.main, userInfo_2.user.getSupertokensUserId(), userInfo_1.getSupertokensUserId(), null, true); // delete User with EmailPassword userId { JsonObject requestBody = new JsonObject(); - requestBody.addProperty("userId", userInfo_1.id); + requestBody.addProperty("userId", userInfo_1.getSupertokensUserId()); JsonObject deleteResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/user/remove", requestBody, 1000, 1000, null, @@ -155,20 +150,20 @@ public void testDeleteUserBehaviorInIntermediateStateWithUser_1sUserId() throws // check that only auth tables for EmailPassword user have been deleted and the userMetadata table entries still // exist { - UserInfo epUser = EmailPassword.getUserUsingId(process.main, userInfo_1.id); + AuthRecipeUserInfo epUser = EmailPassword.getUserUsingId(process.main, userInfo_1.getSupertokensUserId()); assertNull(epUser); - JsonObject epUserMetadata = UserMetadata.getUserMetadata(process.main, userInfo_1.id); + JsonObject epUserMetadata = UserMetadata.getUserMetadata(process.main, userInfo_1.getSupertokensUserId()); assertNotNull(epUserMetadata); assertEquals(epUserMetadata.get("test").getAsString(), "testData"); } // check that the mapping still exists { io.supertokens.pluginInterface.useridmapping.UserIdMapping mapping = UserIdMapping - .getUserIdMapping(process.main, userInfo_2.user.id, UserIdType.ANY); + .getUserIdMapping(process.main, userInfo_2.user.getSupertokensUserId(), UserIdType.ANY); assertNotNull(mapping); - assertEquals(mapping.superTokensUserId, userInfo_2.user.id); - assertEquals(mapping.externalUserId, userInfo_1.id); + assertEquals(mapping.superTokensUserId, userInfo_2.user.getSupertokensUserId()); + assertEquals(mapping.externalUserId, userInfo_1.getSupertokensUserId()); } process.kill(); @@ -187,24 +182,24 @@ public void testDeleteUserBehaviorInIntermediateStateWithUser_2sUserId() throws } // create an EmailPassword User - UserInfo userInfo_1 = EmailPassword.signUp(process.main, "test@example.com", "testPassword123"); + AuthRecipeUserInfo userInfo_1 = EmailPassword.signUp(process.main, "test@example.com", "testPassword123"); // associate some data with user JsonObject data = new JsonObject(); data.addProperty("test", "testData"); - UserMetadata.updateUserMetadata(process.main, userInfo_1.id, data); + UserMetadata.updateUserMetadata(process.main, userInfo_1.getSupertokensUserId(), data); // create a new User who we would like to migrate the EmailPassword user to ThirdParty.SignInUpResponse userInfo_2 = ThirdParty.signInUp(process.main, "google", "test-google", "test123@example.com"); // force create a mapping between the thirdParty user and EmailPassword user - UserIdMapping.createUserIdMapping(process.main, userInfo_2.user.id, userInfo_1.id, null, true); + UserIdMapping.createUserIdMapping(process.main, userInfo_2.user.getSupertokensUserId(), userInfo_1.getSupertokensUserId(), null, true); // delete User with ThirdParty users id { JsonObject requestBody = new JsonObject(); - requestBody.addProperty("userId", userInfo_2.user.id); + requestBody.addProperty("userId", userInfo_2.user.getSupertokensUserId()); JsonObject deleteResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/user/remove", requestBody, 1000, 1000, null, @@ -215,18 +210,18 @@ public void testDeleteUserBehaviorInIntermediateStateWithUser_2sUserId() throws // check that only auth tables for thirdParty user have been deleted and the userMetadata table entries still // exist { - io.supertokens.pluginInterface.thirdparty.UserInfo tpUserInfo = ThirdParty.getUser(process.main, - userInfo_2.user.id); + AuthRecipeUserInfo tpUserInfo = ThirdParty.getUser(process.main, + userInfo_2.user.getSupertokensUserId()); assertNull(tpUserInfo); - JsonObject epUserMetadata = UserMetadata.getUserMetadata(process.main, userInfo_1.id); + JsonObject epUserMetadata = UserMetadata.getUserMetadata(process.main, userInfo_1.getSupertokensUserId()); assertNotNull(epUserMetadata); assertEquals(epUserMetadata.get("test").getAsString(), "testData"); } // check that the mapping is also deleted { io.supertokens.pluginInterface.useridmapping.UserIdMapping mapping = UserIdMapping - .getUserIdMapping(process.main, userInfo_2.user.id, UserIdType.ANY); + .getUserIdMapping(process.main, userInfo_2.user.getSupertokensUserId(), UserIdType.ANY); assertNull(mapping); } diff --git a/src/test/java/io/supertokens/test/authRecipe/GetUserByIdAPITest.java b/src/test/java/io/supertokens/test/authRecipe/GetUserByIdAPITest.java new file mode 100644 index 000000000..8d898c1f3 --- /dev/null +++ b/src/test/java/io/supertokens/test/authRecipe/GetUserByIdAPITest.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.authRecipe; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.webserver.WebserverAPI; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class GetUserByIdAPITest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void getUserSuccess() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("userId", user2.getSupertokensUserId()); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + JsonObject jsonUser = response.get("user").getAsJsonObject(); + assert (jsonUser.get("id").getAsString().equals(user.getSupertokensUserId())); + assert (jsonUser.get("timeJoined").getAsLong() == user.timeJoined); + assert (jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 2); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("test@example.com") || + jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("test2@example.com")); + assert (jsonUser.get("emails").getAsJsonArray().get(1).getAsString().equals("test@example.com") || + jsonUser.get("emails").getAsJsonArray().get(1).getAsString().equals("test2@example.com")); + assert (!jsonUser.get("emails").getAsJsonArray().get(1).getAsString() + .equals(jsonUser.get("emails").getAsJsonArray().get(0).getAsString())); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 0); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 2); + { + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), user.getSupertokensUserId()); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "test@example.com"); + assert (lM.entrySet().size() == 6); + } + { + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(1).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user2.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), user2.getSupertokensUserId()); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "test2@example.com"); + assert (lM.entrySet().size() == 6); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void getUserSuccessWithUserIdMapping() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + UserIdMapping.createUserIdMapping(process.main, user.getSupertokensUserId(), "e1", null, false); + + Thread.sleep(50); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + assert (!user2.isPrimaryUser); + UserIdMapping.createUserIdMapping(process.main, user2.getSupertokensUserId(), "e2", null, false); + + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(), user.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("userId", "e2"); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + JsonObject jsonUser = response.get("user").getAsJsonObject(); + assert (jsonUser.get("id").getAsString().equals("e1")); + assert (jsonUser.get("timeJoined").getAsLong() == user.timeJoined); + assert (jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 2); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("test@example.com") || + jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("test2@example.com")); + assert (jsonUser.get("emails").getAsJsonArray().get(1).getAsString().equals("test@example.com") || + jsonUser.get("emails").getAsJsonArray().get(1).getAsString().equals("test2@example.com")); + assert (!jsonUser.get("emails").getAsJsonArray().get(1).getAsString() + .equals(jsonUser.get("emails").getAsJsonArray().get(0).getAsString())); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 0); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 2); + { + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), "e1"); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "test@example.com"); + assert (lM.entrySet().size() == 6); + } + { + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(1).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user2.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), "e2"); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "test2@example.com"); + assert (lM.entrySet().size() == 6); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void getUserSuccess2() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + assert (!user.isPrimaryUser); + + Thread.sleep(50); + + + ThirdParty.SignInUpResponse signInUpRespone = ThirdParty.signInUp(process.getProcess(), "google", "google-user", + "test@example.com"); + AuthRecipeUserInfo user2 = signInUpRespone.user; + assert (!user2.isPrimaryUser); + + AuthRecipe.createPrimaryUser(process.main, user2.getSupertokensUserId()); + + AuthRecipe.linkAccounts(process.main, user.getSupertokensUserId(), user2.getSupertokensUserId()); + + { + Map params = new HashMap<>(); + params.put("userId", user2.getSupertokensUserId()); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + JsonObject jsonUser = response.get("user").getAsJsonObject(); + assert (jsonUser.get("id").getAsString().equals(user2.getSupertokensUserId())); + assert (jsonUser.get("timeJoined").getAsLong() == user.timeJoined); + assert (jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 1); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("test@example.com")); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 1); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 2); + { + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), user.getSupertokensUserId()); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "test@example.com"); + assert (lM.entrySet().size() == 6); + } + { + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(1).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user2.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), user2.getSupertokensUserId()); + assertEquals(lM.get("recipeId").getAsString(), "thirdparty"); + assertEquals(lM.get("email").getAsString(), "test@example.com"); + assert (lM.entrySet().size() == 7); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void getUnknownUser() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + Map params = new HashMap<>(); + params.put("userId", "random"); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/user/id", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(1, response.entrySet().size()); + assertEquals("UNKNOWN_USER_ID_ERROR", response.get("status").getAsString()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/authRecipe/GetUsersAPIWithUserIdMappingTest.java b/src/test/java/io/supertokens/test/authRecipe/GetUsersAPIWithUserIdMappingTest.java index 0de6afa4e..eca8e44a5 100644 --- a/src/test/java/io/supertokens/test/authRecipe/GetUsersAPIWithUserIdMappingTest.java +++ b/src/test/java/io/supertokens/test/authRecipe/GetUsersAPIWithUserIdMappingTest.java @@ -21,7 +21,7 @@ import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage; import io.supertokens.storageLayer.StorageLayer; @@ -70,8 +70,8 @@ public void createMultipleUsersAndMapTheirIdsRetrieveAllUsersAndCheckThatExterna for (int i = 1; i <= 10; i++) { // create User - UserInfo userInfo = EmailPassword.signUp(process.main, "test" + i + "@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test" + i + "@example.com", "testPass123"); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId" + i; externalUserIdList.add(externalUserId); @@ -110,8 +110,8 @@ public void createMultipleUsersAndMapTheirIdsRetrieveUsersUsingPaginationTokenAn for (int i = 1; i <= 20; i++) { // create User - UserInfo userInfo = EmailPassword.signUp(process.main, "test" + i + "@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test" + i + "@example.com", "testPass123"); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId" + i; externalUserIdList.add(externalUserId); diff --git a/src/test/java/io/supertokens/test/authRecipe/GetUsersWithSearchTagsAPITest.java b/src/test/java/io/supertokens/test/authRecipe/GetUsersWithSearchTagsAPITest.java index 069fc8dec..9dabb17d4 100644 --- a/src/test/java/io/supertokens/test/authRecipe/GetUsersWithSearchTagsAPITest.java +++ b/src/test/java/io/supertokens/test/authRecipe/GetUsersWithSearchTagsAPITest.java @@ -21,7 +21,6 @@ import static org.junit.Assert.assertTrue; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import io.supertokens.utils.SemVer; @@ -32,16 +31,13 @@ import org.junit.rules.TestRule; import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.ProcessState.PROCESS_STATE; -import io.supertokens.dashboard.Dashboard; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.passwordless.Passwordless; import io.supertokens.passwordless.Passwordless.CreateCodeResponse; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -74,17 +70,17 @@ public void testSearchingWhenFieldsHaveEmptyInputsWillBehaveLikeRegularPaginatio // create emailpassword user ArrayList userIds = new ArrayList<>(); - userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").id); + userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").getSupertokensUserId()); // create thirdparty user - userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test2@example.com").user.id); + userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test2@example.com").user.getSupertokensUserId()); // create passwordless user CreateCodeResponse createCodeResponse = Passwordless.createCode(process.getProcess(), "test@example.com", null, null, null); userIds.add(Passwordless.consumeCode(process.getProcess(), createCodeResponse.deviceId, createCodeResponse.deviceIdHash, - createCodeResponse.userInputCode, null).user.id); + createCodeResponse.userInputCode, null).user.getSupertokensUserId()); // search with empty input for email field { @@ -150,18 +146,18 @@ public void testSearchingForUsers() throws Exception { // create emailpassword user ArrayList userIds = new ArrayList<>(); - userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").id); - userIds.add(EmailPassword.signUp(process.getProcess(), "test2@example.com", "testPass123").id); + userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").getSupertokensUserId()); + userIds.add(EmailPassword.signUp(process.getProcess(), "test2@example.com", "testPass123").getSupertokensUserId()); // create thirdparty user - userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test2@example.com").user.id); + userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test2@example.com").user.getSupertokensUserId()); // create passwordless user CreateCodeResponse createCodeResponse = Passwordless.createCode(process.getProcess(), "test@example.com", null, null, null); userIds.add(Passwordless.consumeCode(process.getProcess(), createCodeResponse.deviceId, createCodeResponse.deviceIdHash, - createCodeResponse.userInputCode, null).user.id); + createCodeResponse.userInputCode, null).user.getSupertokensUserId()); // search with partial input for email field HashMap params = new HashMap<>(); @@ -194,9 +190,9 @@ public void testSearchingForUsersWithMultipleInputsForEachField() throws Excepti // create emailpassword user ArrayList userIds = new ArrayList<>(); - userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").id); + userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").getSupertokensUserId()); Thread.sleep(50); - userIds.add(EmailPassword.signUp(process.getProcess(), "abc@example.com", "testPass123").id); + userIds.add(EmailPassword.signUp(process.getProcess(), "abc@example.com", "testPass123").getSupertokensUserId()); Thread.sleep(50); // search with multiple inputs to email @@ -217,9 +213,9 @@ public void testSearchingForUsersWithMultipleInputsForEachField() throws Excepti } // create thirdparty user - userIds.add(ThirdParty.signInUp(process.getProcess(), "testpid", "test", "test@example.com").user.id); + userIds.add(ThirdParty.signInUp(process.getProcess(), "testpid", "test", "test@example.com").user.getSupertokensUserId()); Thread.sleep(50); - userIds.add(ThirdParty.signInUp(process.getProcess(), "newtestpid", "test123", "test@example.com").user.id); + userIds.add(ThirdParty.signInUp(process.getProcess(), "newtestpid", "test123", "test@example.com").user.getSupertokensUserId()); Thread.sleep(50); // search with multiple inputs to provider { @@ -245,7 +241,7 @@ public void testSearchingForUsersWithMultipleInputsForEachField() throws Excepti null, null); userIds.add(Passwordless.consumeCode(process.getProcess(), createCodeResponse.deviceId, createCodeResponse.deviceIdHash, - createCodeResponse.userInputCode, null).user.id); + createCodeResponse.userInputCode, null).user.getSupertokensUserId()); } Thread.sleep(50); { @@ -254,7 +250,7 @@ public void testSearchingForUsersWithMultipleInputsForEachField() throws Excepti null, null); userIds.add(Passwordless.consumeCode(process.getProcess(), createCodeResponse.deviceId, createCodeResponse.deviceIdHash, - createCodeResponse.userInputCode, null).user.id); + createCodeResponse.userInputCode, null).user.getSupertokensUserId()); } Thread.sleep(50); @@ -289,10 +285,10 @@ public void testRetrievingUsersWithConflictingTagsReturnsEmptyList() throws Exce // create emailpassword user ArrayList userIds = new ArrayList<>(); - userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").id); + userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").getSupertokensUserId()); // create thirdparty user - userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test@example.com").user.id); + userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test@example.com").user.getSupertokensUserId()); // create passwordless user CreateCodeResponse createCodeResponse = Passwordless.createCode(process.getProcess(), "test@example.com", @@ -300,7 +296,7 @@ public void testRetrievingUsersWithConflictingTagsReturnsEmptyList() throws Exce null, null); userIds.add(Passwordless.consumeCode(process.getProcess(), createCodeResponse.deviceId, createCodeResponse.deviceIdHash, - createCodeResponse.userInputCode, null).user.id); + createCodeResponse.userInputCode, null).user.getSupertokensUserId()); HashMap params = new HashMap<>(); params.put("email", "test@example.com"); @@ -330,10 +326,10 @@ public void testNormalizingSearchInputsWorksCorrectly() throws Exception { // create emailpassword user ArrayList userIds = new ArrayList<>(); - userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").id); + userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").getSupertokensUserId()); // create thirdparty user - userIds.add(ThirdParty.signInUp(process.getProcess(), "testpid", "test", "test@example.com").user.id); + userIds.add(ThirdParty.signInUp(process.getProcess(), "testpid", "test", "test@example.com").user.getSupertokensUserId()); { // searching for email with upper and lower case combination diff --git a/src/test/java/io/supertokens/test/authRecipe/GetUsersWithSearchTagsTest.java b/src/test/java/io/supertokens/test/authRecipe/GetUsersWithSearchTagsTest.java index 6eef63f9e..4f0246f73 100644 --- a/src/test/java/io/supertokens/test/authRecipe/GetUsersWithSearchTagsTest.java +++ b/src/test/java/io/supertokens/test/authRecipe/GetUsersWithSearchTagsTest.java @@ -31,15 +31,11 @@ import io.supertokens.ProcessState.PROCESS_STATE; import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.authRecipe.UserPaginationContainer; -import io.supertokens.authRecipe.UserPaginationContainer.UsersContainer; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.passwordless.Passwordless; -import io.supertokens.passwordless.Passwordless.ConsumeCodeResponse; import io.supertokens.passwordless.Passwordless.CreateCodeResponse; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; -import io.supertokens.pluginInterface.passwordless.UserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -71,11 +67,11 @@ public void retriveUsersUsingSearchTags() throws Exception { // create emailpassword user ArrayList userIds = new ArrayList<>(); - userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").id); - userIds.add(EmailPassword.signUp(process.getProcess(), "test2@example.com", "testPass123").id); + userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").getSupertokensUserId()); + userIds.add(EmailPassword.signUp(process.getProcess(), "test2@example.com", "testPass123").getSupertokensUserId()); // create thirdparty user - userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test2@example.com").user.id); + userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test2@example.com").user.getSupertokensUserId()); // create passwordless user CreateCodeResponse createCodeResponse = Passwordless.createCode(process.getProcess(), "test@example.com", @@ -83,7 +79,7 @@ public void retriveUsersUsingSearchTags() throws Exception { null, null); userIds.add(Passwordless.consumeCode(process.getProcess(), createCodeResponse.deviceId, createCodeResponse.deviceIdHash, - createCodeResponse.userInputCode, null).user.id); + createCodeResponse.userInputCode, null).user.getSupertokensUserId()); // partial search with input emails as "test" { @@ -95,7 +91,7 @@ public void retriveUsersUsingSearchTags() throws Exception { UserPaginationContainer info = AuthRecipe.getUsers(process.getProcess(), 10, "ASC", null, null, tags); assertEquals(userIds.size(), info.users.length); for (int i = 0; i < info.users.length; i++) { - assertTrue(userIds.contains(info.users[i].user.id)); + assertTrue(userIds.contains(info.users[i].getSupertokensUserId())); } } @@ -109,8 +105,8 @@ public void retriveUsersUsingSearchTags() throws Exception { DashboardSearchTags tags = new DashboardSearchTags(arrayList, null, arrayList); UserPaginationContainer info = AuthRecipe.getUsers(process.getProcess(), 10, "ASC", null, null, tags); assertEquals(1, info.users.length); - assertEquals(userIds.get(2), info.users[0].user.id); - assertEquals("thirdparty", info.users[0].recipeId); + assertEquals(userIds.get(2), info.users[0].getSupertokensUserId()); + assertEquals("thirdparty", info.users[0].loginMethods[0].recipeId.toString()); } @@ -124,8 +120,8 @@ public void retriveUsersUsingSearchTags() throws Exception { DashboardSearchTags tags = new DashboardSearchTags(null, arrayList, null); UserPaginationContainer info = AuthRecipe.getUsers(process.getProcess(), 10, "ASC", null, null, tags); assertEquals(1, info.users.length); - assertEquals(userIds.get(3), info.users[0].user.id); - assertEquals("passwordless", info.users[0].recipeId); + assertEquals(userIds.get(3), info.users[0].getSupertokensUserId()); + assertEquals("passwordless", info.users[0].loginMethods[0].recipeId.toString()); } process.kill(); @@ -144,11 +140,11 @@ public void testRetrievingUsersWithConflictingTagsReturnsEmptyList() throws Exce // create emailpassword user ArrayList userIds = new ArrayList<>(); - userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").id); - userIds.add(EmailPassword.signUp(process.getProcess(), "test2@example.com", "testPass123").id); + userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").getSupertokensUserId()); + userIds.add(EmailPassword.signUp(process.getProcess(), "test2@example.com", "testPass123").getSupertokensUserId()); // create thirdparty user - userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test2@example.com").user.id); + userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test2@example.com").user.getSupertokensUserId()); // create passwordless user CreateCodeResponse createCodeResponse = Passwordless.createCode(process.getProcess(), "test@example.com", @@ -156,7 +152,7 @@ public void testRetrievingUsersWithConflictingTagsReturnsEmptyList() throws Exce null, null); userIds.add(Passwordless.consumeCode(process.getProcess(), createCodeResponse.deviceId, createCodeResponse.deviceIdHash, - createCodeResponse.userInputCode, null).user.id); + createCodeResponse.userInputCode, null).user.getSupertokensUserId()); // test retrieving a user with a phoneNumber and provider { @@ -187,12 +183,12 @@ public void testSearchParamRegex() throws Exception { // create emailpassword user ArrayList userIds = new ArrayList<>(); - userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").id); - userIds.add(EmailPassword.signUp(process.getProcess(), "abc@example.com", "testPass123").id); - userIds.add(EmailPassword.signUp(process.getProcess(), "user@abc.com", "testPass123").id); + userIds.add(EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123").getSupertokensUserId()); + userIds.add(EmailPassword.signUp(process.getProcess(), "abc@example.com", "testPass123").getSupertokensUserId()); + userIds.add(EmailPassword.signUp(process.getProcess(), "user@abc.com", "testPass123").getSupertokensUserId()); // create thirdparty user - userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test2@example.com").user.id); + userIds.add(ThirdParty.signInUp(process.getProcess(), "testTPID", "test", "test2@example.com").user.getSupertokensUserId()); // create passwordless user CreateCodeResponse createCodeResponse = Passwordless.createCode(process.getProcess(), "test@example.com", @@ -200,7 +196,7 @@ public void testSearchParamRegex() throws Exception { null, null); userIds.add(Passwordless.consumeCode(process.getProcess(), createCodeResponse.deviceId, createCodeResponse.deviceIdHash, - createCodeResponse.userInputCode, null).user.id); + createCodeResponse.userInputCode, null).user.getSupertokensUserId()); // regex for emails: email* and *@email* { @@ -214,9 +210,9 @@ public void testSearchParamRegex() throws Exception { DashboardSearchTags tags = new DashboardSearchTags(emailList, null, null); UserPaginationContainer info = AuthRecipe.getUsers(process.getProcess(), 10, "ASC", null, null, tags); assertEquals(3, info.users.length); - assertEquals(userIds.get(0), info.users[0].user.id); - assertEquals(userIds.get(3), info.users[1].user.id); - assertEquals(userIds.get(4), info.users[2].user.id); + assertEquals(userIds.get(0), info.users[0].getSupertokensUserId()); + assertEquals(userIds.get(3), info.users[1].getSupertokensUserId()); + assertEquals(userIds.get(4), info.users[2].getSupertokensUserId()); } // retrieve emails for users whose email starts with abc or have domain abc @@ -229,8 +225,8 @@ public void testSearchParamRegex() throws Exception { DashboardSearchTags tags = new DashboardSearchTags(emailList, null, null); UserPaginationContainer info = AuthRecipe.getUsers(process.getProcess(), 10, "ASC", null, null, tags); assertEquals(2, info.users.length); - assertEquals(userIds.get(1), info.users[0].user.id); - assertEquals(userIds.get(2), info.users[1].user.id); + assertEquals(userIds.get(1), info.users[0].getSupertokensUserId()); + assertEquals(userIds.get(2), info.users[1].getSupertokensUserId()); } @@ -244,7 +240,7 @@ public void testSearchParamRegex() throws Exception { UserPaginationContainer info = AuthRecipe.getUsers(process.getProcess(), 10, "ASC", null, null, tags); assertEquals(1, info.users.length); - assertEquals(userIds.get(4), info.users[0].user.id); + assertEquals(userIds.get(4), info.users[0].getSupertokensUserId()); } } @@ -268,7 +264,7 @@ public void testThatQueryLimitIsCappedAt1000PerTable() throws Exception { ArrayList userIds = new ArrayList<>(); for (int i = 0; i < 1005; i++) { - userIds.add(EmailPassword.signUp(process.getProcess(), "test" + i + "@example.com", "testPass123").id); + userIds.add(EmailPassword.signUp(process.getProcess(), "test" + i + "@example.com", "testPass123").getSupertokensUserId()); Thread.sleep(10); } @@ -280,7 +276,7 @@ public void testThatQueryLimitIsCappedAt1000PerTable() throws Exception { UserPaginationContainer info = AuthRecipe.getUsers(process.getProcess(), 10, "ASC", null, null, tags); assertEquals(1000, info.users.length); for (int i = 0; i < info.users.length; i++) { - assertTrue(userIds.contains(info.users[i].user.id)); + assertTrue(userIds.contains(info.users[i].getSupertokensUserId())); } diff --git a/src/test/java/io/supertokens/test/authRecipe/MultitenantAPITest.java b/src/test/java/io/supertokens/test/authRecipe/MultitenantAPITest.java index 51056e96b..c2e58abfd 100644 --- a/src/test/java/io/supertokens/test/authRecipe/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/authRecipe/MultitenantAPITest.java @@ -21,6 +21,7 @@ import com.google.gson.JsonObject; import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; @@ -30,7 +31,7 @@ import io.supertokens.passwordless.Passwordless; import io.supertokens.passwordless.exceptions.*; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -175,7 +176,8 @@ private void createUsers() throws TenantOrAppNotFoundException, DuplicateEmailException, StorageQueryException, BadPermissionException, DuplicateLinkCodeHashException, NoSuchAlgorithmException, IOException, RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, - StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException, + EmailChangeNotAllowedException { tenantToUsers = new HashMap<>(); recipeToUsers = new HashMap<>(); @@ -187,22 +189,22 @@ private void createUsers() tenantToUsers.put(tenant, new ArrayList<>()); } - UserInfo user1 = EmailPassword.signUp( + AuthRecipeUserInfo user1 = EmailPassword.signUp( tenant.withStorage(StorageLayer.getStorage(tenant, process.getProcess())), process.getProcess(), "user@example.com", "password" + (pcount++) ); - tenantToUsers.get(tenant).add(user1.id); - recipeToUsers.get("emailpassword").add(user1.id); - UserInfo user2 = EmailPassword.signUp( + tenantToUsers.get(tenant).add(user1.getSupertokensUserId()); + recipeToUsers.get("emailpassword").add(user1.getSupertokensUserId()); + AuthRecipeUserInfo user2 = EmailPassword.signUp( tenant.withStorage(StorageLayer.getStorage(tenant, process.getProcess())), process.getProcess(), "user@gmail.com", "password2" + (pcount++) ); - tenantToUsers.get(tenant).add(user2.id); - recipeToUsers.get("emailpassword").add(user2.id); + tenantToUsers.get(tenant).add(user2.getSupertokensUserId()); + recipeToUsers.get("emailpassword").add(user2.getSupertokensUserId()); } } { // passwordless users @@ -225,8 +227,8 @@ private void createUsers() Passwordless.ConsumeCodeResponse response = Passwordless.consumeCode(tenantIdentifierWithStorage, process.getProcess(), codeResponse.deviceId, codeResponse.deviceIdHash, "abcd", null); - tenantToUsers.get(tenant).add(response.user.id); - recipeToUsers.get("passwordless").add(response.user.id); + tenantToUsers.get(tenant).add(response.user.getSupertokensUserId()); + recipeToUsers.get("passwordless").add(response.user.getSupertokensUserId()); } { Passwordless.CreateCodeResponse codeResponse = Passwordless.createCode( @@ -236,10 +238,11 @@ private void createUsers() null, null, "abcd" ); - Passwordless.ConsumeCodeResponse response = Passwordless.consumeCode(tenantIdentifierWithStorage, process.getProcess(), codeResponse.deviceId, + Passwordless.ConsumeCodeResponse response = Passwordless.consumeCode(tenantIdentifierWithStorage, + process.getProcess(), codeResponse.deviceId, codeResponse.deviceIdHash, "abcd", null); - tenantToUsers.get(tenant).add(response.user.id); - recipeToUsers.get("passwordless").add(response.user.id); + tenantToUsers.get(tenant).add(response.user.getSupertokensUserId()); + recipeToUsers.get("passwordless").add(response.user.getSupertokensUserId()); } { Passwordless.CreateCodeResponse codeResponse = Passwordless.createCode( @@ -249,10 +252,11 @@ private void createUsers() "+1234567890", null, "abcd" ); - Passwordless.ConsumeCodeResponse response = Passwordless.consumeCode(tenantIdentifierWithStorage, process.getProcess(), codeResponse.deviceId, + Passwordless.ConsumeCodeResponse response = Passwordless.consumeCode(tenantIdentifierWithStorage, + process.getProcess(), codeResponse.deviceId, codeResponse.deviceIdHash, "abcd", null); - tenantToUsers.get(tenant).add(response.user.id); - recipeToUsers.get("passwordless").add(response.user.id); + tenantToUsers.get(tenant).add(response.user.getSupertokensUserId()); + recipeToUsers.get("passwordless").add(response.user.getSupertokensUserId()); } { Passwordless.CreateCodeResponse codeResponse = Passwordless.createCode( @@ -262,10 +266,11 @@ private void createUsers() "+9876543210", null, "abcd" ); - Passwordless.ConsumeCodeResponse response = Passwordless.consumeCode(tenantIdentifierWithStorage, process.getProcess(), codeResponse.deviceId, + Passwordless.ConsumeCodeResponse response = Passwordless.consumeCode(tenantIdentifierWithStorage, + process.getProcess(), codeResponse.deviceId, codeResponse.deviceIdHash, "abcd", null); - tenantToUsers.get(tenant).add(response.user.id); - recipeToUsers.get("passwordless").add(response.user.id); + tenantToUsers.get(tenant).add(response.user.getSupertokensUserId()); + recipeToUsers.get("passwordless").add(response.user.getSupertokensUserId()); } } } @@ -282,28 +287,28 @@ private void createUsers() ThirdParty.SignInUpResponse user1 = ThirdParty.signInUp(tenantIdentifierWithStorage, process.getProcess(), "google", "googleid1", "user@example.com"); - tenantToUsers.get(tenant).add(user1.user.id); - recipeToUsers.get("thirdparty").add(user1.user.id); + tenantToUsers.get(tenant).add(user1.user.getSupertokensUserId()); + recipeToUsers.get("thirdparty").add(user1.user.getSupertokensUserId()); ThirdParty.SignInUpResponse user2 = ThirdParty.signInUp(tenantIdentifierWithStorage, process.getProcess(), "google", "googleid2", "user@gmail.com"); - tenantToUsers.get(tenant).add(user2.user.id); - recipeToUsers.get("thirdparty").add(user2.user.id); + tenantToUsers.get(tenant).add(user2.user.getSupertokensUserId()); + recipeToUsers.get("thirdparty").add(user2.user.getSupertokensUserId()); ThirdParty.SignInUpResponse user3 = ThirdParty.signInUp(tenantIdentifierWithStorage, process.getProcess(), "facebook", "facebookid1", "user@example.com"); - tenantToUsers.get(tenant).add(user3.user.id); - recipeToUsers.get("thirdparty").add(user3.user.id); + tenantToUsers.get(tenant).add(user3.user.getSupertokensUserId()); + recipeToUsers.get("thirdparty").add(user3.user.getSupertokensUserId()); ThirdParty.SignInUpResponse user4 = ThirdParty.signInUp(tenantIdentifierWithStorage, process.getProcess(), "facebook", "facebookid2", "user@gmail.com"); - tenantToUsers.get(tenant).add(user4.user.id); - recipeToUsers.get("thirdparty").add(user4.user.id); + tenantToUsers.get(tenant).add(user4.user.getSupertokensUserId()); + recipeToUsers.get("thirdparty").add(user4.user.getSupertokensUserId()); } } } - private long getUserCount(TenantIdentifier tenantIdentifier, String []recipeIds, boolean includeAllTenants) + private long getUserCount(TenantIdentifier tenantIdentifier, String[] recipeIds, boolean includeAllTenants) throws HttpResponseException, IOException { HashMap params = new HashMap<>(); if (recipeIds != null) { @@ -323,7 +328,7 @@ private long getUserCount(TenantIdentifier tenantIdentifier, String []recipeIds, return response.get("count").getAsLong(); } - private String[] getUsers(TenantIdentifier tenantIdentifier, String []recipeIds) + private String[] getUsers(TenantIdentifier tenantIdentifier, String[] recipeIds) throws HttpResponseException, IOException { HashMap params = new HashMap<>(); if (recipeIds != null) { @@ -347,7 +352,8 @@ private String[] getUsers(TenantIdentifier tenantIdentifier, String []recipeIds) return userIds; } - private String[] getUsers(TenantIdentifier tenantIdentifier, String[] emails, String[] phoneNumbers, String[] providers) + private String[] getUsers(TenantIdentifier tenantIdentifier, String[] emails, String[] phoneNumbers, + String[] providers) throws HttpResponseException, IOException { HashMap params = new HashMap<>(); if (emails != null) { @@ -447,7 +453,8 @@ public void testGetUsers() throws Exception { String[] users = getUsers(tenant, new String[]{"emailpassword", "passwordless"}); for (String user : users) { assertTrue(tenantToUsers.get(tenant).contains(user)); - assertTrue(recipeToUsers.get("emailpassword").contains(user) || recipeToUsers.get("passwordless").contains(user)); + assertTrue(recipeToUsers.get("emailpassword").contains(user) || + recipeToUsers.get("passwordless").contains(user)); } } @@ -455,7 +462,8 @@ public void testGetUsers() throws Exception { String[] users = getUsers(tenant, new String[]{"thirdparty", "passwordless"}); for (String user : users) { assertTrue(tenantToUsers.get(tenant).contains(user)); - assertTrue(recipeToUsers.get("thirdparty").contains(user) || recipeToUsers.get("passwordless").contains(user)); + assertTrue(recipeToUsers.get("thirdparty").contains(user) || + recipeToUsers.get("passwordless").contains(user)); } } } diff --git a/src/test/java/io/supertokens/test/authRecipe/UserPaginationTest.java b/src/test/java/io/supertokens/test/authRecipe/UserPaginationTest.java index 5db09f1f6..0e7f5e8d5 100644 --- a/src/test/java/io/supertokens/test/authRecipe/UserPaginationTest.java +++ b/src/test/java/io/supertokens/test/authRecipe/UserPaginationTest.java @@ -21,6 +21,7 @@ import com.google.gson.JsonObject; import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; @@ -30,7 +31,7 @@ import io.supertokens.passwordless.Passwordless; import io.supertokens.passwordless.exceptions.*; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -44,13 +45,18 @@ import io.supertokens.test.multitenant.api.TestMultitenancyAPIHelper; import io.supertokens.thirdparty.InvalidProviderConfigException; import io.supertokens.thirdparty.ThirdParty; -import org.junit.*; -import org.junit.rules.TestRule; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; import static org.junit.Assert.*; @@ -168,23 +174,25 @@ private void createUsers(TenantIdentifier tenantIdentifier, int numUsers, String throws TenantOrAppNotFoundException, DuplicateEmailException, StorageQueryException, BadPermissionException, DuplicateLinkCodeHashException, NoSuchAlgorithmException, IOException, RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, - StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException, + EmailChangeNotAllowedException { if (tenantToUsers.get(tenantIdentifier) == null) { tenantToUsers.put(tenantIdentifier, new ArrayList<>()); } - TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, process.getProcess())); + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage( + StorageLayer.getStorage(tenantIdentifier, process.getProcess())); for (int i = 0; i < numUsers; i++) { { - UserInfo user = EmailPassword.signUp( + AuthRecipeUserInfo user = EmailPassword.signUp( tenantIdentifierWithStorage, process.getProcess(), prefix + "epuser" + i + "@example.com", "password" + i); - tenantToUsers.get(tenantIdentifier).add(user.id); + tenantToUsers.get(tenantIdentifier).add(user.getSupertokensUserId()); if (!recipeToUsers.containsKey("emailpassword")) { recipeToUsers.put("emailpassword", new ArrayList<>()); } - recipeToUsers.get("emailpassword").add(user.id); + recipeToUsers.get("emailpassword").add(user.getSupertokensUserId()); } { Passwordless.CreateCodeResponse codeResponse = Passwordless.createCode( @@ -197,27 +205,27 @@ private void createUsers(TenantIdentifier tenantIdentifier, int numUsers, String Passwordless.ConsumeCodeResponse response = Passwordless.consumeCode(tenantIdentifierWithStorage, process.getProcess(), codeResponse.deviceId, codeResponse.deviceIdHash, "abcd", null); - tenantToUsers.get(tenantIdentifier).add(response.user.id); + tenantToUsers.get(tenantIdentifier).add(response.user.getSupertokensUserId()); if (!recipeToUsers.containsKey("passwordless")) { recipeToUsers.put("passwordless", new ArrayList<>()); } - recipeToUsers.get("passwordless").add(response.user.id); + recipeToUsers.get("passwordless").add(response.user.getSupertokensUserId()); } { ThirdParty.SignInUpResponse user1 = ThirdParty.signInUp(tenantIdentifierWithStorage, process.getProcess(), "google", "googleid" + i, prefix + "tpuser" + i + "@example.com"); - tenantToUsers.get(tenantIdentifier).add(user1.user.id); + tenantToUsers.get(tenantIdentifier).add(user1.user.getSupertokensUserId()); ThirdParty.SignInUpResponse user2 = ThirdParty.signInUp(tenantIdentifierWithStorage, process.getProcess(), "facebook", "fbid" + i, prefix + "tpuser" + i + "@example.com"); - tenantToUsers.get(tenantIdentifier).add(user2.user.id); + tenantToUsers.get(tenantIdentifier).add(user2.user.getSupertokensUserId()); if (!recipeToUsers.containsKey("thirdparty")) { recipeToUsers.put("thirdparty", new ArrayList<>()); } - recipeToUsers.get("thirdparty").add(user1.user.id); - recipeToUsers.get("thirdparty").add(user2.user.id); + recipeToUsers.get("thirdparty").add(user1.user.getSupertokensUserId()); + recipeToUsers.get("thirdparty").add(user2.user.getSupertokensUserId()); } } } @@ -241,7 +249,8 @@ public void testUserPaginationWorksCorrectlyForEachTenant() throws Exception { { // All recipes Set userIdSet = new HashSet<>(); - JsonObject userList = TestMultitenancyAPIHelper.listUsers(tenantIdentifier, null, "10", null, process.getProcess()); + JsonObject userList = TestMultitenancyAPIHelper.listUsers(tenantIdentifier, null, "10", null, + process.getProcess()); String paginationToken = userList.get("nextPaginationToken").getAsString(); JsonArray users = userList.get("users").getAsJsonArray(); @@ -254,7 +263,8 @@ public void testUserPaginationWorksCorrectlyForEachTenant() throws Exception { } while (paginationToken != null) { - userList = TestMultitenancyAPIHelper.listUsers(tenantIdentifier, paginationToken, "10", null, process.getProcess()); + userList = TestMultitenancyAPIHelper.listUsers(tenantIdentifier, paginationToken, "10", null, + process.getProcess()); users = userList.get("users").getAsJsonArray(); for (JsonElement user : users) { @@ -275,7 +285,8 @@ public void testUserPaginationWorksCorrectlyForEachTenant() throws Exception { } { // recipe combinations - String[] combinations = new String[]{"emailpassword", "passwordless", "thirdparty", "emailpassword,passwordless", "emailpassword,thirdparty", "passwordless,thirdparty"}; + String[] combinations = new String[]{"emailpassword", "passwordless", "thirdparty", + "emailpassword,passwordless", "emailpassword,thirdparty", "passwordless,thirdparty"}; int[] userCounts = new int[]{50, 50, 100, 100, 150, 150}; for (int i = 0; i < combinations.length; i++) { @@ -284,7 +295,8 @@ public void testUserPaginationWorksCorrectlyForEachTenant() throws Exception { Set userIdSet = new HashSet<>(); - JsonObject userList = TestMultitenancyAPIHelper.listUsers(tenantIdentifier, null, "10", includeRecipeIds, process.getProcess()); + JsonObject userList = TestMultitenancyAPIHelper.listUsers(tenantIdentifier, null, "10", + includeRecipeIds, process.getProcess()); String paginationToken = userList.get("nextPaginationToken").getAsString(); JsonArray users = userList.get("users").getAsJsonArray(); @@ -300,11 +312,13 @@ public void testUserPaginationWorksCorrectlyForEachTenant() throws Exception { } while (paginationToken != null) { - userList = TestMultitenancyAPIHelper.listUsers(tenantIdentifier, paginationToken, "10", includeRecipeIds, process.getProcess()); + userList = TestMultitenancyAPIHelper.listUsers(tenantIdentifier, paginationToken, "10", + includeRecipeIds, process.getProcess()); users = userList.get("users").getAsJsonArray(); for (JsonElement user : users) { - String userId = user.getAsJsonObject().get("user").getAsJsonObject().get("id").getAsString(); + String userId = user.getAsJsonObject().get("user").getAsJsonObject().get("id") + .getAsString(); String recipeId = user.getAsJsonObject().get("recipeId").getAsString(); assertFalse(userIdSet.contains(userId)); userIdSet.add(userId); diff --git a/src/test/java/io/supertokens/test/emailpassword/DeleteExpiredPasswordResetTokensCronjobTest.java b/src/test/java/io/supertokens/test/emailpassword/DeleteExpiredPasswordResetTokensCronjobTest.java index 4bbb630d5..63959abb4 100644 --- a/src/test/java/io/supertokens/test/emailpassword/DeleteExpiredPasswordResetTokensCronjobTest.java +++ b/src/test/java/io/supertokens/test/emailpassword/DeleteExpiredPasswordResetTokensCronjobTest.java @@ -21,8 +21,8 @@ import io.supertokens.cronjobs.deleteExpiredPasswordResetTokens.DeleteExpiredPasswordResetTokens; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo; -import io.supertokens.pluginInterface.emailpassword.UserInfo; import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.storageLayer.StorageLayer; @@ -64,23 +64,23 @@ public void checkingCronJob() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); - String tok = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); - String tok2 = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); + String tok = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); + String tok2 = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); Thread.sleep(2000); - String tok3 = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); - String tok4 = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); + String tok3 = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); + String tok4 = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); assert (((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.id).length == 4); + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()).length == 4); Thread.sleep(3500); PasswordResetTokenInfo[] tokens = ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.id); + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()); assert (tokens.length == 2); diff --git a/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java b/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java index c1346d096..b9f9387bc 100644 --- a/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java +++ b/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java @@ -16,26 +16,32 @@ package io.supertokens.test.emailpassword; +import com.google.gson.JsonObject; import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.config.Config; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.emailpassword.PasswordHashing; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.emailpassword.exceptions.ResetPasswordInvalidTokenException; import io.supertokens.emailpassword.exceptions.WrongCredentialsException; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.multitenancy.Multitenancy; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo; -import io.supertokens.pluginInterface.emailpassword.UserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicatePasswordResetTokenException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage; -import io.supertokens.pluginInterface.multitenancy.AppIdentifier; -import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; +import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; +import io.supertokens.thirdparty.ThirdParty; import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; @@ -79,12 +85,14 @@ public void testStorageLayerGetMailPasswordStorageLayerThrowsExceptionIfTypeIsNo if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { try { - new TenantIdentifierWithStorage(null, null, null, StorageLayer.getStorage(process.getProcess())).getEmailPasswordStorage(); + new TenantIdentifierWithStorage(null, null, null, + StorageLayer.getStorage(process.getProcess())).getEmailPasswordStorage(); throw new Exception("Should not come here"); } catch (UnsupportedOperationException e) { } } else { - new TenantIdentifierWithStorage(null, null, null, StorageLayer.getStorage(process.getProcess())).getEmailPasswordStorage(); + new TenantIdentifierWithStorage(null, null, null, + StorageLayer.getStorage(process.getProcess())).getEmailPasswordStorage(); } process.kill(); @@ -139,12 +147,13 @@ public void testResetPasswordToken() throws Exception { return; } - UserInfo userInfo = EmailPassword.signUp(process.getProcess(), "random@gmail.com", "validPass123"); - assertEquals(userInfo.email, "random@gmail.com"); - assertNotNull(userInfo.id); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.getProcess(), "random@gmail.com", "validPass123"); + assertEquals(userInfo.loginMethods[0].email, "random@gmail.com"); + assertNotNull(userInfo.getSupertokensUserId()); for (int i = 0; i < 100; i++) { - String generatedResetToken = EmailPassword.generatePasswordResetToken(process.getProcess(), userInfo.id); + String generatedResetToken = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), + userInfo.getSupertokensUserId()); assertEquals(generatedResetToken.length(), 128); assertFalse(generatedResetToken.contains("+")); @@ -169,13 +178,13 @@ public void testThatAfterSignUpThePasswordIsHashedAndStoredInTheDatabase() throw return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "random@gmail.com", "validPass123"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "random@gmail.com", "validPass123"); - UserInfo userInfo = ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getUserInfoUsingEmail(new TenantIdentifier(null, null, null), user.email); - assertNotEquals(userInfo.passwordHash, "validPass123"); + AuthRecipeUserInfo userInfo = ((AuthRecipeStorage) StorageLayer.getStorage(process.getProcess())) + .listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), user.loginMethods[0].email)[0]; + assertNotEquals(userInfo.loginMethods[0].passwordHash, "validPass123"); assertTrue(PasswordHashing.getInstance(process.getProcess()).verifyPasswordWithHash("validPass123", - userInfo.passwordHash)); + userInfo.loginMethods[0].passwordHash)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -195,10 +204,11 @@ public void testThatAfterResetPasswordGenerateTokenTheTokenIsHashedInTheDatabase return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "random@gmail.com", "validPass123"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "random@gmail.com", "validPass123"); - String resetToken = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); - PasswordResetTokenInfo resetTokenInfo = ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) + String resetToken = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); + PasswordResetTokenInfo resetTokenInfo = ((EmailPasswordSQLStorage) StorageLayer.getStorage( + process.getProcess())) .getPasswordResetTokenInfo(new AppIdentifier(null, null), io.supertokens.utils.Utils.hashSHA256(resetToken)); @@ -223,18 +233,18 @@ public void testThatAfterResetPasswordIsCompletedThePasswordIsHashedInTheDatabas return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "random@gmail.com", "validPass123"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "random@gmail.com", "validPass123"); - String resetToken = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); + String resetToken = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); EmailPassword.resetPassword(process.getProcess(), resetToken, "newValidPass123"); - UserInfo userInfo = ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getUserInfoUsingEmail(new TenantIdentifier(null, null, null), user.email); - assertNotEquals(userInfo.passwordHash, "newValidPass123"); + AuthRecipeUserInfo userInfo = ((AuthRecipeStorage) StorageLayer.getStorage(process.getProcess())) + .listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), user.loginMethods[0].email)[0]; + assertNotEquals(userInfo.loginMethods[0].passwordHash, "newValidPass123"); assertTrue(PasswordHashing.getInstance(process.getProcess()).verifyPasswordWithHash("newValidPass123", - userInfo.passwordHash)); + userInfo.loginMethods[0].passwordHash)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -253,12 +263,12 @@ public void passwordResetTokenExpiredCheck() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); - String tok = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); + String tok = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); assert (((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.id).length == 1); + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()).length == 1); Thread.sleep(20); @@ -270,7 +280,7 @@ public void passwordResetTokenExpiredCheck() throws Exception { } assert (((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.id).length == 0); + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()).length == 0); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -287,21 +297,21 @@ public void multiplePasswordResetTokensPerUserAndThenVerifyWithSignin() throws E return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); - EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); - String tok = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); - EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); + EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); + String tok = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); + EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); PasswordResetTokenInfo[] tokens = ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.id); + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()); assert (tokens.length == 3); EmailPassword.resetPassword(process.getProcess(), tok, "newPassword"); tokens = ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.id); + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()); assert (tokens.length == 0); try { @@ -311,9 +321,9 @@ public void multiplePasswordResetTokensPerUserAndThenVerifyWithSignin() throws E } - UserInfo user1 = EmailPassword.signIn(process.getProcess(), "test1@example.com", "newPassword"); + AuthRecipeUserInfo user1 = EmailPassword.signIn(process.getProcess(), "test1@example.com", "newPassword"); - assertEquals(user1.email, user.email); + assertEquals(user1.loginMethods[0].email, user.loginMethods[0].email); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -373,19 +383,19 @@ public void clashingPassowordResetToken() throws Exception { } // we add a user first. - UserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) .addPasswordResetToken(new AppIdentifier(null, null), new PasswordResetTokenInfo( - user.id, "token", + user.getSupertokensUserId(), "token", System.currentTimeMillis() + - Config.getConfig(process.getProcess()).getPasswordResetTokenLifetime())); + Config.getConfig(process.getProcess()).getPasswordResetTokenLifetime(), "email")); try { ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) .addPasswordResetToken(new AppIdentifier(null, null), - new PasswordResetTokenInfo(user.id, "token", System.currentTimeMillis() - + Config.getConfig(process.getProcess()).getPasswordResetTokenLifetime())); + new PasswordResetTokenInfo(user.getSupertokensUserId(), "token", System.currentTimeMillis() + + Config.getConfig(process.getProcess()).getPasswordResetTokenLifetime(), "email")); assert (false); } catch (DuplicatePasswordResetTokenException ignored) { @@ -407,7 +417,8 @@ public void unknownUserIdWhileGeneratingPasswordResetToken() throws Exception { } try { - EmailPassword.generatePasswordResetToken(process.getProcess(), "8ed86166-bfd8-4234-9dfe-abca9606dbd5"); + EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), + "8ed86166-bfd8-4234-9dfe-abca9606dbd5"); assert (false); } catch (UnknownUserIdException ignored) { @@ -437,7 +448,7 @@ public void clashingUserIdDuringSignUp() throws Exception { ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) .signUp(new TenantIdentifier(null, null, null), "8ed86166-bfd8-4234-9dfe-abca9606dbd5", "test1@example.com", "password", - System.currentTimeMillis()); + System.currentTimeMillis()); assert (false); } catch (DuplicateUserIdException ignored) { @@ -490,7 +501,7 @@ public void clashingEmailAndUserIdDuringSignUp() throws Exception { ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) .signUp(new TenantIdentifier(null, null, null), "8ed86166-bfd8-4234-9dfe-abca9606dbd5", "test@example.com", "password", - System.currentTimeMillis()); + System.currentTimeMillis()); assert (false); } catch (DuplicateUserIdException ignored) { @@ -511,13 +522,13 @@ public void signUpAndThenSignIn() throws Exception { return; } - UserInfo userSignUp = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + AuthRecipeUserInfo userSignUp = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); - UserInfo user = EmailPassword.signIn(process.getProcess(), "test@example.com", "password"); + AuthRecipeUserInfo user = EmailPassword.signIn(process.getProcess(), "test@example.com", "password"); - assert (user.email.equals("test@example.com")); + assert (user.loginMethods[0].email.equals("test@example.com")); - assert (userSignUp.id.equals(user.id)); + assert (userSignUp.getSupertokensUserId().equals(user.getSupertokensUserId())); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -604,4 +615,387 @@ public void changePasswordResetLifetimeTest() throws Exception { assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } } + + @Test + public void testGeneratingResetPasswordTokenForNonEPUserNonPrimary() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google", + "user-google", + "test@example.com"); + + try { + EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.main, signInUpResponse.user.getSupertokensUserId()); + assert false; + } catch (UnknownUserIdException ignored) { + + } + + String token = EmailPassword.generatePasswordResetToken(process.main, signInUpResponse.user.getSupertokensUserId(), + "test@example.com"); + + EmailPassword.ConsumeResetPasswordTokenResult res = EmailPassword.consumeResetPasswordToken(process.main, + token); + assert (res.email.equals("test@example.com")); + assert (res.userId.equals(signInUpResponse.user.getSupertokensUserId())); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testGeneratingResetPasswordTokenForNonEPUserPrimary() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google", + "user-google", + "test@example.com"); + + AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId()); + + String token = EmailPassword.generatePasswordResetToken(process.main, signInUpResponse.user.getSupertokensUserId(), + "test@example.com"); + + EmailPassword.ConsumeResetPasswordTokenResult res = EmailPassword.consumeResetPasswordToken(process.main, + token); + assert (res.email.equals("test@example.com")); + assert (res.userId.equals(signInUpResponse.user.getSupertokensUserId())); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testGeneratingResetPasswordTokenForNonEPUserPrimaryButDeletedWithOtherLinked() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google", + "user-google", + "test@example.com"); + + ThirdParty.SignInUpResponse signInUpResponse2 = ThirdParty.signInUp(process.getProcess(), "fb", + "user-fb", + "test2@example.com"); + + AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, signInUpResponse2.user.getSupertokensUserId(), signInUpResponse.user.getSupertokensUserId()); + assert (AuthRecipe.unlinkAccounts(process.main, signInUpResponse.user.getSupertokensUserId())); + + String token = EmailPassword.generatePasswordResetToken(process.main, signInUpResponse.user.getSupertokensUserId(), + "test@example.com"); + + EmailPassword.ConsumeResetPasswordTokenResult res = EmailPassword.consumeResetPasswordToken(process.main, + token); + assert (res.email.equals("test@example.com")); + assert (res.userId.equals(signInUpResponse.user.getSupertokensUserId())); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void deletionOfTpUserDeletesPasswordResetToken() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google", + "user-google", + "test@example.com"); + + + String token = EmailPassword.generatePasswordResetToken(process.main, signInUpResponse.user.getSupertokensUserId(), + "test@example.com"); + token = io.supertokens.utils.Utils.hashSHA256(token); + + assertNotNull(((EmailPasswordSQLStorage) StorageLayer.getStorage(process.main)).getPasswordResetTokenInfo( + new AppIdentifier(null, null), token)); + + AuthRecipe.deleteUser(process.main, signInUpResponse.user.getSupertokensUserId()); + + assertNull(((EmailPasswordSQLStorage) StorageLayer.getStorage(process.main)).getPasswordResetTokenInfo( + new AppIdentifier(null, null), token)); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void passwordResetTokenExpiredCheckWithConsumeCode() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + Utils.setValueInConfig("password_reset_token_lifetime", "10"); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + + String tok = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); + + assert (((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()).length == 1); + + Thread.sleep(20); + + try { + EmailPassword.consumeResetPasswordToken(process.getProcess(), tok); + assert (false); + } catch (ResetPasswordInvalidTokenException ignored) { + + } + + assert (((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()).length == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void multiplePasswordResetTokensPerUserAndThenVerifyWithSigninWithConsumeCode() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + + EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); + String tok = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); + EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); + + PasswordResetTokenInfo[] tokens = ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()); + + assert (tokens.length == 3); + + EmailPassword.consumeResetPasswordToken(process.getProcess(), tok); + + tokens = ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()); + assert (tokens.length == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void wrongPasswordResetTokenWithConsumeCode() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + try { + EmailPassword.consumeResetPasswordToken(process.getProcess(), "token"); + assert (false); + } catch (ResetPasswordInvalidTokenException ignored) { + + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void consumeCodeCorrectlySetsTheUserEmailForOlderTokens() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + + String tok = EmailPassword.generatePasswordResetTokenBeforeCdi4_0WithoutAddingEmail(process.getProcess(), + user.getSupertokensUserId()); + + assert (((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()).length == 1); + assert (((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId())[0].email == null); + + EmailPassword.ConsumeResetPasswordTokenResult result = EmailPassword.consumeResetPasswordToken( + process.getProcess(), tok); + assert (result.email.equals("test1@example.com")); + assert (result.userId.equals(user.getSupertokensUserId())); + + assert (((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) + .getAllPasswordResetTokenInfoForUser(new AppIdentifier(null, null), user.getSupertokensUserId()).length == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + + @Test + public void updateEmailFailsIfEmailUsedByOtherPrimaryUserInSameTenant() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user0 = EmailPassword.signUp(process.getProcess(), "someemail1@gmail.com", "somePass"); + AuthRecipe.createPrimaryUser(process.main, user0.getSupertokensUserId()); + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + try { + EmailPassword.updateUsersEmailOrPassword(process.main, user.getSupertokensUserId(), "someemail1@gmail.com", null); + assert (false); + } catch (EmailChangeNotAllowedException ignored) { + + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void updateEmailSucceedsIfEmailUsedByOtherPrimaryUserInDifferentTenantWhichThisUserIsNotAPartOf() + throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), + new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true), + new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(true), + new JsonObject())); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", + StorageLayer.getStorage(process.main)); + AuthRecipeUserInfo user0 = EmailPassword.signUp(tenantIdentifierWithStorage, process.getProcess(), + "someemail1@gmail.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, user0.getSupertokensUserId()); + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + EmailPassword.updateUsersEmailOrPassword(process.main, user.getSupertokensUserId(), "someemail1@gmail.com", null); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void updateEmailFailsIfEmailUsedByOtherPrimaryUserInDifferentTenant() + throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null), + new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true), + new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(true), + new JsonObject())); + + TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", + StorageLayer.getStorage(process.main)); + AuthRecipeUserInfo user0 = EmailPassword.signUp(tenantIdentifierWithStorage, process.getProcess(), + "someemail1@gmail.com", + "pass1234"); + + AuthRecipe.createPrimaryUser(process.main, user0.getSupertokensUserId()); + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + Multitenancy.addUserIdToTenant(process.main, tenantIdentifierWithStorage, user.getSupertokensUserId()); + + try { + EmailPassword.updateUsersEmailOrPassword(process.main, user.getSupertokensUserId(), "someemail1@gmail.com", null); + assert (false); + } catch (EmailChangeNotAllowedException ignored) { + + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/src/test/java/io/supertokens/test/emailpassword/MultitenantEmailPasswordTest.java b/src/test/java/io/supertokens/test/emailpassword/MultitenantEmailPasswordTest.java index ad9e631d0..80a6fbfe1 100644 --- a/src/test/java/io/supertokens/test/emailpassword/MultitenantEmailPasswordTest.java +++ b/src/test/java/io/supertokens/test/emailpassword/MultitenantEmailPasswordTest.java @@ -19,6 +19,7 @@ import com.google.gson.JsonObject; import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.emailpassword.exceptions.WrongCredentialsException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; @@ -28,7 +29,7 @@ import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.Storage; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; @@ -47,7 +48,8 @@ import java.io.IOException; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; public class MultitenantEmailPasswordTest { @AfterClass @@ -157,20 +159,23 @@ public void testSignUpAndLoginInDifferentTenants() { EmailPassword.signUp(t1storage, process.getProcess(), "user1@example.com", "password1"); - UserInfo userInfo = EmailPassword.signIn(t1storage, process.getProcess(), "user1@example.com", "password1"); - assertEquals("user1@example.com", userInfo.email); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(t1storage, process.getProcess(), "user1@example.com", + "password1"); + assertEquals("user1@example.com", userInfo.loginMethods[0].email); } { EmailPassword.signUp(t2storage, process.getProcess(), "user2@example.com", "password2"); - UserInfo userInfo = EmailPassword.signIn(t2storage, process.getProcess(), "user2@example.com", "password2"); - assertEquals("user2@example.com", userInfo.email); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(t2storage, process.getProcess(), "user2@example.com", + "password2"); + assertEquals("user2@example.com", userInfo.loginMethods[0].email); } { EmailPassword.signUp(t3storage, process.getProcess(), "user3@example.com", "password3"); - UserInfo userInfo = EmailPassword.signIn(t3storage, process.getProcess(), "user3@example.com", "password3"); - assertEquals("user3@example.com", userInfo.email); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(t3storage, process.getProcess(), "user3@example.com", + "password3"); + assertEquals("user3@example.com", userInfo.loginMethods[0].email); } process.kill(); @@ -210,18 +215,21 @@ public void testSameEmailWithDifferentPasswordsOnDifferentTenantsWorksCorrectly( EmailPassword.signUp(t3storage, process.getProcess(), "user@example.com", "password3"); { - UserInfo userInfo = EmailPassword.signIn(t1storage, process.getProcess(), "user@example.com", "password1"); - assertEquals("user@example.com", userInfo.email); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(t1storage, process.getProcess(), "user@example.com", + "password1"); + assertEquals("user@example.com", userInfo.loginMethods[0].email); } { - UserInfo userInfo = EmailPassword.signIn(t2storage, process.getProcess(), "user@example.com", "password2"); - assertEquals("user@example.com", userInfo.email); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(t2storage, process.getProcess(), "user@example.com", + "password2"); + assertEquals("user@example.com", userInfo.loginMethods[0].email); } { - UserInfo userInfo = EmailPassword.signIn(t3storage, process.getProcess(), "user@example.com", "password3"); - assertEquals("user@example.com", userInfo.email); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(t3storage, process.getProcess(), "user@example.com", + "password3"); + assertEquals("user@example.com", userInfo.loginMethods[0].email); } process.kill(); @@ -255,33 +263,33 @@ public void testGetUserUsingIdReturnsCorrectUser() TenantIdentifier t3 = new TenantIdentifier(null, "a1", "t2"); TenantIdentifierWithStorage t3storage = t3.withStorage(StorageLayer.getStorage(t3, process.getProcess())); - UserInfo user1 = EmailPassword.signUp(t1storage, process.getProcess(), "user1@example.com", "password1"); - UserInfo user2 = EmailPassword.signUp(t2storage, process.getProcess(), "user2@example.com", "password2"); - UserInfo user3 = EmailPassword.signUp(t3storage, process.getProcess(), "user3@example.com", "password3"); + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1storage, process.getProcess(), "user1@example.com", "password1"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(t2storage, process.getProcess(), "user2@example.com", "password2"); + AuthRecipeUserInfo user3 = EmailPassword.signUp(t3storage, process.getProcess(), "user3@example.com", "password3"); Storage storage = StorageLayer.getStorage(process.getProcess()); { - UserInfo userInfo = EmailPassword.getUserUsingId( + AuthRecipeUserInfo userInfo = EmailPassword.getUserUsingId( StorageLayer.getAppIdentifierWithStorageAndUserIdMappingForUserWithPriorityForTenantStorage( - process.getProcess(), new AppIdentifier(null, "a1"), storage, user1.id, - UserIdType.SUPERTOKENS).appIdentifierWithStorage, user1.id); + process.getProcess(), new AppIdentifier(null, "a1"), storage, user1.getSupertokensUserId(), + UserIdType.SUPERTOKENS).appIdentifierWithStorage, user1.getSupertokensUserId()); assertEquals(user1, userInfo); } { - UserInfo userInfo = EmailPassword.getUserUsingId( + AuthRecipeUserInfo userInfo = EmailPassword.getUserUsingId( StorageLayer.getAppIdentifierWithStorageAndUserIdMappingForUserWithPriorityForTenantStorage( - process.getProcess(), new AppIdentifier(null, "a1"), storage, user2.id, - UserIdType.SUPERTOKENS).appIdentifierWithStorage, user2.id); + process.getProcess(), new AppIdentifier(null, "a1"), storage, user2.getSupertokensUserId(), + UserIdType.SUPERTOKENS).appIdentifierWithStorage, user2.getSupertokensUserId()); assertEquals(user2, userInfo); } { - UserInfo userInfo = EmailPassword.getUserUsingId( + AuthRecipeUserInfo userInfo = EmailPassword.getUserUsingId( StorageLayer.getAppIdentifierWithStorageAndUserIdMappingForUserWithPriorityForTenantStorage( - process.getProcess(), new AppIdentifier(null, "a1"), storage, user3.id, - UserIdType.SUPERTOKENS).appIdentifierWithStorage, user3.id); + process.getProcess(), new AppIdentifier(null, "a1"), storage, user3.getSupertokensUserId(), + UserIdType.SUPERTOKENS).appIdentifierWithStorage, user3.getSupertokensUserId()); assertEquals(user3, userInfo); } @@ -315,22 +323,22 @@ public void testGetUserUsingEmailReturnsTheUserFromTheSpecificTenant() TenantIdentifier t3 = new TenantIdentifier(null, "a1", "t2"); TenantIdentifierWithStorage t3storage = t3.withStorage(StorageLayer.getStorage(t3, process.getProcess())); - UserInfo user1 = EmailPassword.signUp(t1storage, process.getProcess(), "user@example.com", "password1"); - UserInfo user2 = EmailPassword.signUp(t2storage, process.getProcess(), "user@example.com", "password2"); - UserInfo user3 = EmailPassword.signUp(t3storage, process.getProcess(), "user@example.com", "password3"); + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1storage, process.getProcess(), "user@example.com", "password1"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(t2storage, process.getProcess(), "user@example.com", "password2"); + AuthRecipeUserInfo user3 = EmailPassword.signUp(t3storage, process.getProcess(), "user@example.com", "password3"); { - UserInfo userInfo = EmailPassword.getUserUsingEmail(t1storage, user1.email); + AuthRecipeUserInfo userInfo = EmailPassword.getUserUsingEmail(t1storage, user1.loginMethods[0].email); assertEquals(user1, userInfo); } { - UserInfo userInfo = EmailPassword.getUserUsingEmail(t2storage, user2.email); + AuthRecipeUserInfo userInfo = EmailPassword.getUserUsingEmail(t2storage, user2.loginMethods[0].email); assertEquals(user2, userInfo); } { - UserInfo userInfo = EmailPassword.getUserUsingEmail(t3storage, user3.email); + AuthRecipeUserInfo userInfo = EmailPassword.getUserUsingEmail(t3storage, user3.loginMethods[0].email); assertEquals(user3, userInfo); } @@ -343,7 +351,8 @@ public void testUpdatePasswordWorksCorrectlyAcrossAllTenants() throws InterruptedException, InvalidProviderConfigException, StorageQueryException, FeatureNotEnabledException, TenantOrAppNotFoundException, IOException, InvalidConfigException, CannotModifyBaseConfigException, BadPermissionException, DuplicateEmailException, - UnknownUserIdException, StorageTransactionLogicException, WrongCredentialsException { + UnknownUserIdException, StorageTransactionLogicException, WrongCredentialsException, + EmailChangeNotAllowedException { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); @@ -365,47 +374,50 @@ public void testUpdatePasswordWorksCorrectlyAcrossAllTenants() TenantIdentifier t3 = new TenantIdentifier(null, "a1", "t2"); TenantIdentifierWithStorage t3storage = t3.withStorage(StorageLayer.getStorage(t3, process.getProcess())); - UserInfo user1 = EmailPassword.signUp(t1storage, process.getProcess(), "user@example.com", "password1"); - UserInfo user2 = EmailPassword.signUp(t2storage, process.getProcess(), "user@example.com", "password2"); - UserInfo user3 = EmailPassword.signUp(t3storage, process.getProcess(), "user@example.com", "password3"); + AuthRecipeUserInfo user1 = EmailPassword.signUp(t1storage, process.getProcess(), "user@example.com", "password1"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(t2storage, process.getProcess(), "user@example.com", "password2"); + AuthRecipeUserInfo user3 = EmailPassword.signUp(t3storage, process.getProcess(), "user@example.com", "password3"); Storage storage = StorageLayer.getStorage(process.getProcess()); EmailPassword.updateUsersEmailOrPassword( StorageLayer.getAppIdentifierWithStorageAndUserIdMappingForUserWithPriorityForTenantStorage( - process.getProcess(), new AppIdentifier(null, "a1"), storage, user1.id, + process.getProcess(), new AppIdentifier(null, "a1"), storage, user1.getSupertokensUserId(), UserIdType.SUPERTOKENS).appIdentifierWithStorage, - process.getProcess(), user1.id, null, "newpassword1"); + process.getProcess(), user1.getSupertokensUserId(), null, "newpassword1"); EmailPassword.updateUsersEmailOrPassword( StorageLayer.getAppIdentifierWithStorageAndUserIdMappingForUserWithPriorityForTenantStorage( - process.getProcess(), new AppIdentifier(null, "a1"), storage, user2.id, + process.getProcess(), new AppIdentifier(null, "a1"), storage, user2.getSupertokensUserId(), UserIdType.SUPERTOKENS).appIdentifierWithStorage, - process.getProcess(), user2.id, null, "newpassword2"); + process.getProcess(), user2.getSupertokensUserId(), null, "newpassword2"); EmailPassword.updateUsersEmailOrPassword( StorageLayer.getAppIdentifierWithStorageAndUserIdMappingForUserWithPriorityForTenantStorage( - process.getProcess(), new AppIdentifier(null, "a1"), storage, user3.id, + process.getProcess(), new AppIdentifier(null, "a1"), storage, user3.getSupertokensUserId(), UserIdType.SUPERTOKENS).appIdentifierWithStorage, - process.getProcess(), user3.id, null, "newpassword3"); + process.getProcess(), user3.getSupertokensUserId(), null, "newpassword3"); { - t1 = StorageLayer.getTenantIdentifierWithStorageAndUserIdMappingForUser(process.getProcess(), t1, user1.id, + t1 = StorageLayer.getTenantIdentifierWithStorageAndUserIdMappingForUser(process.getProcess(), t1, user1.getSupertokensUserId(), UserIdType.SUPERTOKENS).tenantIdentifierWithStorage; - UserInfo userInfo = EmailPassword.signIn(t1storage, process.getProcess(), "user@example.com", "newpassword1"); - assertEquals(user1.id, userInfo.id); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(t1storage, process.getProcess(), "user@example.com", + "newpassword1"); + assertEquals(user1.getSupertokensUserId(), userInfo.getSupertokensUserId()); } { - t2 = StorageLayer.getTenantIdentifierWithStorageAndUserIdMappingForUser(process.getProcess(), t2, user2.id, + t2 = StorageLayer.getTenantIdentifierWithStorageAndUserIdMappingForUser(process.getProcess(), t2, user2.getSupertokensUserId(), UserIdType.SUPERTOKENS).tenantIdentifierWithStorage; - UserInfo userInfo = EmailPassword.signIn(t2storage, process.getProcess(), "user@example.com", "newpassword2"); - assertEquals(user2.id, userInfo.id); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(t2storage, process.getProcess(), "user@example.com", + "newpassword2"); + assertEquals(user2.getSupertokensUserId(), userInfo.getSupertokensUserId()); } { - t3 = StorageLayer.getTenantIdentifierWithStorageAndUserIdMappingForUser(process.getProcess(), t3, user3.id, + t3 = StorageLayer.getTenantIdentifierWithStorageAndUserIdMappingForUser(process.getProcess(), t3, user3.getSupertokensUserId(), UserIdType.SUPERTOKENS).tenantIdentifierWithStorage; - UserInfo userInfo = EmailPassword.signIn(t3storage, process.getProcess(), "user@example.com", "newpassword3"); - assertEquals(user3.id, userInfo.id); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(t3storage, process.getProcess(), "user@example.com", + "newpassword3"); + assertEquals(user3.getSupertokensUserId(), userInfo.getSupertokensUserId()); } process.kill(); diff --git a/src/test/java/io/supertokens/test/emailpassword/PasswordHashingTest.java b/src/test/java/io/supertokens/test/emailpassword/PasswordHashingTest.java index f34bb3084..683d99001 100644 --- a/src/test/java/io/supertokens/test/emailpassword/PasswordHashingTest.java +++ b/src/test/java/io/supertokens/test/emailpassword/PasswordHashingTest.java @@ -25,7 +25,7 @@ import io.supertokens.emailpassword.exceptions.WrongCredentialsException; import io.supertokens.inmemorydb.Start; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; @@ -88,9 +88,9 @@ public void importUserWithFireBaseSCrypt() throws Exception { CoreConfig.PASSWORD_HASHING_ALG.FIREBASE_SCRYPT); // try signing in - UserInfo user = EmailPassword.signIn(process.main, email, password); - assertEquals(user.email, email); - assertEquals(user.passwordHash, combinedPasswordHash); + AuthRecipeUserInfo user = EmailPassword.signIn(process.main, email, password); + assertEquals(user.loginMethods[0].email, email); + assertEquals(user.loginMethods[0].passwordHash, combinedPasswordHash); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -522,14 +522,14 @@ public void hashAndVerifyWithBcryptChangeToArgonPasswordWithResetFlow() throws E return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "t@example.com", "somePass"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "t@example.com", "somePass"); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.PASSWORD_HASH_BCRYPT)); ProcessState.getInstance(process.getProcess()).clear(); Config.getConfig(process.getProcess()).setPasswordHashingAlg(CoreConfig.PASSWORD_HASHING_ALG.ARGON2); - String token = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); + String token = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); EmailPassword.resetPassword(process.getProcess(), token, "somePass2"); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.PASSWORD_HASH_ARGON)); @@ -553,14 +553,14 @@ public void hashAndVerifyWithArgonChangeToBcryptPasswordWithResetFlow() throws E return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "t@example.com", "somePass"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "t@example.com", "somePass"); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.PASSWORD_HASH_ARGON)); ProcessState.getInstance(process.getProcess()).clear(); Config.getConfig(process.getProcess()).setPasswordHashingAlg(CoreConfig.PASSWORD_HASHING_ALG.BCRYPT); - String token = EmailPassword.generatePasswordResetToken(process.getProcess(), user.id); + String token = EmailPassword.generatePasswordResetTokenBeforeCdi4_0(process.getProcess(), user.getSupertokensUserId()); EmailPassword.resetPassword(process.getProcess(), token, "somePass2"); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.PASSWORD_HASH_BCRYPT)); @@ -583,14 +583,14 @@ public void hashAndVerifyWithBcryptChangeToArgonChangePassword() throws Exceptio return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "t@example.com", "somePass"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "t@example.com", "somePass"); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.PASSWORD_HASH_BCRYPT)); ProcessState.getInstance(process.getProcess()).clear(); Config.getConfig(process.getProcess()).setPasswordHashingAlg(CoreConfig.PASSWORD_HASHING_ALG.ARGON2); - EmailPassword.updateUsersEmailOrPassword(process.getProcess(), user.id, null, "somePass2"); + EmailPassword.updateUsersEmailOrPassword(process.getProcess(), user.getSupertokensUserId(), null, "somePass2"); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.PASSWORD_HASH_ARGON)); @@ -613,14 +613,14 @@ public void hashAndVerifyWithArgonChangeToBcryptChangePassword() throws Exceptio return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "t@example.com", "somePass"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "t@example.com", "somePass"); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.PASSWORD_HASH_ARGON)); ProcessState.getInstance(process.getProcess()).clear(); Config.getConfig(process.getProcess()).setPasswordHashingAlg(CoreConfig.PASSWORD_HASHING_ALG.BCRYPT); - EmailPassword.updateUsersEmailOrPassword(process.getProcess(), user.id, null, "somePass2"); + EmailPassword.updateUsersEmailOrPassword(process.getProcess(), user.getSupertokensUserId(), null, "somePass2"); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.PASSWORD_HASH_BCRYPT)); @@ -721,8 +721,8 @@ public void parallelImportUserSignInFirebaseScrypt() throws Exception { EmailPassword.importUserWithPasswordHash(process.main, uniqueEmail, combinedPasswordHash, CoreConfig.PASSWORD_HASHING_ALG.FIREBASE_SCRYPT); // try signing in - UserInfo user = EmailPassword.signIn(process.main, uniqueEmail, password); - assertEquals(user.passwordHash, combinedPasswordHash); + AuthRecipeUserInfo user = EmailPassword.signIn(process.main, uniqueEmail, password); + assertEquals(user.loginMethods[0].passwordHash, combinedPasswordHash); assertNotNull(process .checkOrWaitForEvent(ProcessState.PROCESS_STATE.PASSWORD_VERIFY_FIREBASE_SCRYPT)); int queueSize = PasswordHashing.getInstance(process.getProcess()) @@ -803,8 +803,8 @@ public void parallelImportUserSignInFirebaseScryptWithPoolSize4() throws Excepti EmailPassword.importUserWithPasswordHash(process.main, uniqueEmail, combinedPasswordHash, CoreConfig.PASSWORD_HASHING_ALG.FIREBASE_SCRYPT); // try signing in - UserInfo user = EmailPassword.signIn(process.main, uniqueEmail, password); - assertEquals(user.passwordHash, combinedPasswordHash); + AuthRecipeUserInfo user = EmailPassword.signIn(process.main, uniqueEmail, password); + assertEquals(user.loginMethods[0].passwordHash, combinedPasswordHash); assertNotNull(process .checkOrWaitForEvent(ProcessState.PROCESS_STATE.PASSWORD_VERIFY_FIREBASE_SCRYPT)); int queueSize = PasswordHashing.getInstance(process.getProcess()) diff --git a/src/test/java/io/supertokens/test/emailpassword/UpdateUsersEmailAndPasswordTest.java b/src/test/java/io/supertokens/test/emailpassword/UpdateUsersEmailAndPasswordTest.java index aa62b47b1..aea6505ee 100644 --- a/src/test/java/io/supertokens/test/emailpassword/UpdateUsersEmailAndPasswordTest.java +++ b/src/test/java/io/supertokens/test/emailpassword/UpdateUsersEmailAndPasswordTest.java @@ -19,7 +19,7 @@ import io.supertokens.Main; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.storageLayer.StorageLayer; @@ -71,16 +71,16 @@ public void testUpdateEmailOnly() throws Exception { } // given - UserInfo userInfo = EmailPassword.signUp(main, "john.doe@example.com", "password"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(main, "john.doe@example.com", "password"); // when - EmailPassword.updateUsersEmailOrPassword(main, userInfo.id, "dave.doe@example.com", null); + EmailPassword.updateUsersEmailOrPassword(main, userInfo.getSupertokensUserId(), "dave.doe@example.com", null); // then - UserInfo changedEmailUserInfo = EmailPassword.signIn(main, "dave.doe@example.com", "password"); + AuthRecipeUserInfo changedEmailUserInfo = EmailPassword.signIn(main, "dave.doe@example.com", "password"); - Assert.assertEquals(userInfo.id, changedEmailUserInfo.id); - Assert.assertEquals("dave.doe@example.com", changedEmailUserInfo.email); + Assert.assertEquals(userInfo.getSupertokensUserId(), changedEmailUserInfo.getSupertokensUserId()); + Assert.assertEquals("dave.doe@example.com", changedEmailUserInfo.loginMethods[0].email); }); } @@ -94,12 +94,12 @@ public void testUpdateEmailToAnotherThatAlreadyExists() throws Exception { } // given - UserInfo userInfo = EmailPassword.signUp(main, "john.doe@example.com", "password"); - UserInfo userInfo2 = EmailPassword.signUp(main, "john.doe1@example.com", "password"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(main, "john.doe@example.com", "password"); + AuthRecipeUserInfo userInfo2 = EmailPassword.signUp(main, "john.doe1@example.com", "password"); // when try { - EmailPassword.updateUsersEmailOrPassword(main, userInfo.id, userInfo2.email, null); + EmailPassword.updateUsersEmailOrPassword(main, userInfo.getSupertokensUserId(), userInfo2.loginMethods[0].email, null); Assert.fail(); } catch (DuplicateEmailException ignored) { } @@ -117,15 +117,15 @@ public void testUpdatePasswordOnly() throws Exception { } // given - UserInfo userInfo = EmailPassword.signUp(main, "john.doe@example.com", "password"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(main, "john.doe@example.com", "password"); // when - EmailPassword.updateUsersEmailOrPassword(main, userInfo.id, null, "newPassword"); + EmailPassword.updateUsersEmailOrPassword(main, userInfo.getSupertokensUserId(), null, "newPassword"); // then - UserInfo changedEmailUserInfo = EmailPassword.signIn(main, "john.doe@example.com", "newPassword"); + AuthRecipeUserInfo changedEmailUserInfo = EmailPassword.signIn(main, "john.doe@example.com", "newPassword"); - Assert.assertEquals(userInfo.id, changedEmailUserInfo.id); + Assert.assertEquals(userInfo.getSupertokensUserId(), changedEmailUserInfo.getSupertokensUserId()); }); } @@ -139,16 +139,17 @@ public void testUpdateEmailAndPassword() throws Exception { } // given - UserInfo userInfo = EmailPassword.signUp(main, "john.doe@example.com", "password"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(main, "john.doe@example.com", "password"); // when - EmailPassword.updateUsersEmailOrPassword(main, userInfo.id, "dave.doe@example.com", "newPassword"); + EmailPassword.updateUsersEmailOrPassword(main, userInfo.getSupertokensUserId(), "dave.doe@example.com", "newPassword"); // then - UserInfo changedCredentialsUserInfo = EmailPassword.signIn(main, "dave.doe@example.com", "newPassword"); + AuthRecipeUserInfo changedCredentialsUserInfo = EmailPassword.signIn(main, "dave.doe@example.com", + "newPassword"); - Assert.assertEquals(userInfo.id, changedCredentialsUserInfo.id); - Assert.assertEquals("dave.doe@example.com", changedCredentialsUserInfo.email); + Assert.assertEquals(userInfo.getSupertokensUserId(), changedCredentialsUserInfo.getSupertokensUserId()); + Assert.assertEquals("dave.doe@example.com", changedCredentialsUserInfo.loginMethods[0].email); }); } } diff --git a/src/test/java/io/supertokens/test/emailpassword/UserMigrationTest.java b/src/test/java/io/supertokens/test/emailpassword/UserMigrationTest.java index 1f14f489d..dea91f847 100644 --- a/src/test/java/io/supertokens/test/emailpassword/UserMigrationTest.java +++ b/src/test/java/io/supertokens/test/emailpassword/UserMigrationTest.java @@ -20,10 +20,9 @@ import io.supertokens.config.CoreConfig.PASSWORD_HASHING_ALG; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.emailpassword.ParsedFirebaseSCryptResponse; -import io.supertokens.emailpassword.PasswordHashingUtils; import io.supertokens.emailpassword.exceptions.WrongCredentialsException; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -51,7 +50,7 @@ public void beforeEach() { @Test public void testSigningInUsersWithDifferentHashingConfigValues() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; Utils.setValueInConfig("firebase_password_hashing_signer_key", "gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/SKa0JDPFeM6TcEevkaW56sIWfx88OHbJKnCXdWscZx0l2WbCJ1wbg=="); @@ -72,16 +71,17 @@ public void testSigningInUsersWithDifferentHashingConfigValues() throws Exceptio String email = "test@example.com"; String password = "testPass123"; String salt = "/cj0jC1br5o4+w=="; - String passwordHash = "9Y8ICWcqbzmI42DxV1jpyEjbrJPG8EQ6nI6oC32JYz+/dd7aEjI/R7jG9P5kYh8v9gyqFKaXMDzMg7eLCypbOA=="; + String passwordHash = "9Y8ICWcqbzmI42DxV1jpyEjbrJPG8EQ6nI6oC32JYz+/dd7aEjI" + + "/R7jG9P5kYh8v9gyqFKaXMDzMg7eLCypbOA=="; String combinedPasswordHash = "$" + ParsedFirebaseSCryptResponse.FIREBASE_SCRYPT_PREFIX + "$" + passwordHash + "$" + salt + "$m=" + firebaseMemCost + "$r=" + firebaseRounds + "$s=" + firebaseSaltSeparator; EmailPassword.importUserWithPasswordHash(process.getProcess(), email, combinedPasswordHash, PASSWORD_HASHING_ALG.FIREBASE_SCRYPT); // try signing in and check that it works - UserInfo userInfo = EmailPassword.signIn(process.getProcess(), email, password); - assertEquals(userInfo.email, email); - assertEquals(userInfo.passwordHash, combinedPasswordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.getProcess(), email, password); + assertEquals(userInfo.loginMethods[0].email, email); + assertEquals(userInfo.loginMethods[0].passwordHash, combinedPasswordHash); } // user 2 has a password hash that was generated with mem cost as 15 @@ -93,7 +93,8 @@ public void testSigningInUsersWithDifferentHashingConfigValues() throws Exceptio String email = "test2@example.com"; String password = "testPass123"; String salt = "/cj0jC1br5o4+w=="; - String passwordHash = "LalFtzCxLIl14+ol6e/3cjHoa2B73ULiMN+Mjm+nJJEfQqtsXPpDX1VU4s9XyiuwGrQ5RN69PWL5DrHuNUH+RA=="; + String passwordHash = "LalFtzCxLIl14+ol6e/3cjHoa2B73ULiMN+Mjm" + + "+nJJEfQqtsXPpDX1VU4s9XyiuwGrQ5RN69PWL5DrHuNUH+RA=="; String combinedPasswordHash = "$" + ParsedFirebaseSCryptResponse.FIREBASE_SCRYPT_PREFIX + "$" + passwordHash + "$" + salt + "$m=" + firebaseMemCost + "$r=" + firebaseRounds + "$s=" + firebaseSaltSeparator; @@ -101,9 +102,9 @@ public void testSigningInUsersWithDifferentHashingConfigValues() throws Exceptio PASSWORD_HASHING_ALG.FIREBASE_SCRYPT); // try signing in and check that it works - UserInfo userInfo = EmailPassword.signIn(process.getProcess(), email, password); - assertEquals(userInfo.email, email); - assertEquals(userInfo.passwordHash, combinedPasswordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.getProcess(), email, password); + assertEquals(userInfo.loginMethods[0].email, email); + assertEquals(userInfo.loginMethods[0].passwordHash, combinedPasswordHash); } process.kill(); @@ -112,7 +113,7 @@ public void testSigningInUsersWithDifferentHashingConfigValues() throws Exceptio @Test public void testBasicUserMigration() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -133,11 +134,11 @@ public void testBasicUserMigration() throws Exception { // check that the user was created assertFalse(importUserResponse.didUserAlreadyExist); // try and sign in with plainTextPassword - UserInfo userInfo = EmailPassword.signIn(process.main, email, plainTextPassword); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, plainTextPassword); - assertEquals(userInfo.id, importUserResponse.user.id); - assertEquals(userInfo.passwordHash, passwordHash); - assertEquals(userInfo.email, email); + assertEquals(userInfo.getSupertokensUserId(), importUserResponse.user.getSupertokensUserId()); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); + assertEquals(userInfo.loginMethods[0].email, email); } // with argon2 @@ -152,11 +153,11 @@ public void testBasicUserMigration() throws Exception { // check that the user was created assertFalse(importUserResponse.didUserAlreadyExist); // try and sign in with plainTextPassword - UserInfo userInfo = EmailPassword.signIn(process.main, email, plainTextPassword); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, plainTextPassword); - assertEquals(userInfo.id, importUserResponse.user.id); - assertEquals(userInfo.passwordHash, passwordHash); - assertEquals(userInfo.email, email); + assertEquals(userInfo.getSupertokensUserId(), importUserResponse.user.getSupertokensUserId()); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); + assertEquals(userInfo.loginMethods[0].email, email); } process.kill(); @@ -165,7 +166,7 @@ public void testBasicUserMigration() throws Exception { @Test public void testUpdatingAUsersPasswordHash() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -177,7 +178,7 @@ public void testUpdatingAUsersPasswordHash() throws Exception { String email = "test@example.com"; String originalPassword = "testPass123"; - UserInfo signUpUserInfo = EmailPassword.signUp(process.main, email, originalPassword); + AuthRecipeUserInfo signUpUserInfo = EmailPassword.signUp(process.main, email, originalPassword); // update passwordHash with new passwordHash String newPassword = "newTestPass123"; @@ -198,11 +199,11 @@ public void testUpdatingAUsersPasswordHash() throws Exception { assertNotNull(error); // sign in with the newPassword and check that it works - UserInfo userInfo = EmailPassword.signIn(process.main, email, newPassword); - assertEquals(userInfo.email, signUpUserInfo.email); - assertEquals(userInfo.id, signUpUserInfo.id); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, newPassword); + assertEquals(userInfo.loginMethods[0].email, signUpUserInfo.loginMethods[0].email); + assertEquals(userInfo.getSupertokensUserId(), signUpUserInfo.getSupertokensUserId()); assertEquals(userInfo.timeJoined, signUpUserInfo.timeJoined); - assertEquals(userInfo.passwordHash, newPasswordHash); + assertEquals(userInfo.loginMethods[0].passwordHash, newPasswordHash); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -211,7 +212,7 @@ public void testUpdatingAUsersPasswordHash() throws Exception { // test bcrypt with different salt rounds @Test public void testAddingBcryptHashesWithDifferentSaltRounds() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -231,9 +232,9 @@ public void testAddingBcryptHashesWithDifferentSaltRounds() throws Exception { assertFalse(response.didUserAlreadyExist); // test that sign in works - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(userInfo.email, email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(userInfo.loginMethods[0].email, email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -241,7 +242,7 @@ public void testAddingBcryptHashesWithDifferentSaltRounds() throws Exception { @Test public void testUsingArgon2HashesWithDifferentConfigValues() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -261,9 +262,9 @@ public void testUsingArgon2HashesWithDifferentConfigValues() throws Exception { assertFalse(response.didUserAlreadyExist); // test that sign in works - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(userInfo.email, email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(userInfo.loginMethods[0].email, email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -271,7 +272,7 @@ public void testUsingArgon2HashesWithDifferentConfigValues() throws Exception { @Test public void testAddingArgon2WithDifferentVersions() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -291,9 +292,9 @@ public void testAddingArgon2WithDifferentVersions() throws Exception { assertFalse(response.didUserAlreadyExist); // test that sign in works - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(userInfo.email, email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(userInfo.loginMethods[0].email, email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); } // $argon2d @@ -307,9 +308,9 @@ public void testAddingArgon2WithDifferentVersions() throws Exception { assertFalse(response.didUserAlreadyExist); // test that sign in works - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(userInfo.email, email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(userInfo.loginMethods[0].email, email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); } process.kill(); @@ -318,7 +319,7 @@ public void testAddingArgon2WithDifferentVersions() throws Exception { @Test public void testAddingBcryptHashesWithDifferentVersions() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -339,9 +340,9 @@ public void testAddingBcryptHashesWithDifferentVersions() throws Exception { assertFalse(response.didUserAlreadyExist); // test that sign in works - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(userInfo.email, email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(userInfo.loginMethods[0].email, email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); } // using $2b$ @@ -355,9 +356,9 @@ public void testAddingBcryptHashesWithDifferentVersions() throws Exception { assertFalse(response.didUserAlreadyExist); // test that sign in works - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(userInfo.email, email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(userInfo.loginMethods[0].email, email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); } // using $2x$ @@ -371,9 +372,9 @@ public void testAddingBcryptHashesWithDifferentVersions() throws Exception { assertFalse(response.didUserAlreadyExist); // test that sign in works - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(userInfo.email, email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(userInfo.loginMethods[0].email, email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); } // using $2y$ @@ -387,9 +388,9 @@ public void testAddingBcryptHashesWithDifferentVersions() throws Exception { assertFalse(response.didUserAlreadyExist); // test that sign in works - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(userInfo.email, email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(userInfo.loginMethods[0].email, email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); } process.kill(); diff --git a/src/test/java/io/supertokens/test/emailpassword/api/ConsumeResetPasswordAPITest4_0.java b/src/test/java/io/supertokens/test/emailpassword/api/ConsumeResetPasswordAPITest4_0.java new file mode 100644 index 000000000..bb973959d --- /dev/null +++ b/src/test/java/io/supertokens/test/emailpassword/api/ConsumeResetPasswordAPITest4_0.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.emailpassword.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + +public class ConsumeResetPasswordAPITest4_0 { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + // Check for bad input (missing fields) + @Test + public void testBadInput() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token/consume", null, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + throw new Exception("Should not come here"); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertTrue(e.statusCode == 400 + && e.getMessage().equals("Http error. Status Code: 400. Message: Invalid Json Input")); + } + } + + { + JsonObject requestBody = new JsonObject(); + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token/consume", requestBody, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + throw new Exception("Should not come here"); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertTrue(e.statusCode == 400 && e.getMessage().equals( + "Http error. Status Code: 400. Message: Field name 'token' is invalid in " + "JSON input")); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + // Check good input works + @Test + public void testGoodInput() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "random@gmail.com", "validPass123"); + + String userId = user.getSupertokensUserId(); + + String token = EmailPassword.generatePasswordResetToken(process.main, userId, "random@gmail.com"); + + JsonObject resetPasswordBody = new JsonObject(); + resetPasswordBody.addProperty("token", token); + + JsonObject passwordResetResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token/consume", resetPasswordBody, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + assertEquals(passwordResetResponse.get("status").getAsString(), "OK"); + assertEquals(passwordResetResponse.get("email").getAsString(), "random@gmail.com"); + assertEquals(passwordResetResponse.get("userId").getAsString(), user.getSupertokensUserId()); + assertEquals(passwordResetResponse.entrySet().size(), 3); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testGoodInputWithUserIdMapping() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "random@gmail.com", "validPass123"); + UserIdMapping.createUserIdMapping(process.main, user.getSupertokensUserId(), "e1", null, false); + + String userId = user.getSupertokensUserId(); + + String token = EmailPassword.generatePasswordResetToken(process.main, userId, "random@gmail.com"); + + JsonObject resetPasswordBody = new JsonObject(); + resetPasswordBody.addProperty("token", token); + + JsonObject passwordResetResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token/consume", resetPasswordBody, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + assertEquals(passwordResetResponse.get("status").getAsString(), "OK"); + assertEquals(passwordResetResponse.get("email").getAsString(), "random@gmail.com"); + assertEquals(passwordResetResponse.get("userId").getAsString(), "e1"); + assertEquals(passwordResetResponse.entrySet().size(), 3); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + // Check for all types of output + // Failure condition: passing a valid password reset token will fail the test + @Test + public void testALLTypesOfOutPut() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject resetPasswordBody = new JsonObject(); + resetPasswordBody.addProperty("token", "randomToken"); + + JsonObject passwordResetResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token/consume", resetPasswordBody, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + + assertEquals(passwordResetResponse.get("status").getAsString(), "RESET_PASSWORD_INVALID_TOKEN_ERROR"); + assertEquals(passwordResetResponse.entrySet().size(), 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/emailpassword/api/EmailPasswordGetUserAPITest2_7.java b/src/test/java/io/supertokens/test/emailpassword/api/EmailPasswordGetUserAPITest2_7.java index 4cbb5f559..ee71b3b44 100644 --- a/src/test/java/io/supertokens/test/emailpassword/api/EmailPasswordGetUserAPITest2_7.java +++ b/src/test/java/io/supertokens/test/emailpassword/api/EmailPasswordGetUserAPITest2_7.java @@ -18,11 +18,14 @@ import com.google.gson.JsonObject; import io.supertokens.ProcessState; +import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; import io.supertokens.utils.SemVer; import org.junit.AfterClass; import org.junit.Before; @@ -194,4 +197,44 @@ public void testForAllTypesOfOutput() throws Exception { process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testGetUserForUsersOfOtherRecipeIds() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = ThirdParty.signInUp(process.getProcess(), "google", "googleid", "test@example.com").user; + Passwordless.CreateCodeResponse user2code = Passwordless.createCode(process.getProcess(), "test@example.com", + null, null, null); + AuthRecipeUserInfo user2 = Passwordless.consumeCode(process.getProcess(), user2code.deviceId, user2code.deviceIdHash, user2code.userInputCode, null).user; + + { + HashMap map = new HashMap<>(); + map.put("userId", user1.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user", map, 1000, 1000, null, SemVer.v2_7.get(), + "emailpassword"); + assertEquals(response.get("status").getAsString(), "UNKNOWN_USER_ID_ERROR"); + } + + { + HashMap map = new HashMap<>(); + map.put("userId", user2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user", map, 1000, 1000, null, SemVer.v2_7.get(), + "emailpassword"); + assertEquals(response.get("status").getAsString(), "UNKNOWN_USER_ID_ERROR"); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/src/test/java/io/supertokens/test/emailpassword/api/GeneratePasswordResetTokenAPITest2_7.java b/src/test/java/io/supertokens/test/emailpassword/api/GeneratePasswordResetTokenAPITest2_7.java index 765602a61..44f6c0de9 100644 --- a/src/test/java/io/supertokens/test/emailpassword/api/GeneratePasswordResetTokenAPITest2_7.java +++ b/src/test/java/io/supertokens/test/emailpassword/api/GeneratePasswordResetTokenAPITest2_7.java @@ -23,6 +23,7 @@ import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; import io.supertokens.utils.SemVer; import org.junit.AfterClass; import org.junit.Before; @@ -50,7 +51,7 @@ public void beforeEach() { // Check for bad input (missing fields) @Test public void testBadInput() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -106,7 +107,7 @@ public void testBadInput() throws Exception { // Check good input works @Test public void testGoodInput() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -139,7 +140,7 @@ public void testGoodInput() throws Exception { // Failure condition: passing a valid userId will cause the test to fail @Test public void testForAllTypesOfOutput() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -161,4 +162,31 @@ public void testForAllTypesOfOutput() throws Exception { process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testUnknownUserWithUserIdFromNonEp() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + ThirdParty.SignInUpResponse res = ThirdParty.signInUp(process.main, "google", "ug", "t@example.com"); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("userId", res.user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token", requestBody, 1000, 1000, null, + SemVer.v2_7.get(), "emailpassword"); + + assertEquals(response.get("status").getAsString(), "UNKNOWN_USER_ID_ERROR"); + assertEquals(response.entrySet().size(), 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/src/test/java/io/supertokens/test/emailpassword/api/GeneratePasswordResetTokenAPITest4_0.java b/src/test/java/io/supertokens/test/emailpassword/api/GeneratePasswordResetTokenAPITest4_0.java new file mode 100644 index 000000000..5e5a96814 --- /dev/null +++ b/src/test/java/io/supertokens/test/emailpassword/api/GeneratePasswordResetTokenAPITest4_0.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.emailpassword.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + +public class GeneratePasswordResetTokenAPITest4_0 { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + // Check for bad input (missing fields) + @Test + public void testBadInput() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token", null, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + throw new Exception("Should not come here"); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertTrue(e.statusCode == 400 + && e.getMessage().equals("Http error. Status Code: 400. Message: Invalid Json Input")); + } + } + + { + JsonObject requestBody = new JsonObject(); + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token", requestBody, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + throw new Exception("Should not come here"); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertTrue(e.statusCode == 400 && e.getMessage().equals( + "Http error. Status Code: 400. Message: Field name 'userId' is invalid in " + "JSON input")); + + } + } + + { + JsonObject requestBody = new JsonObject(); + requestBody.add("userId", null); + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token", requestBody, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + throw new Exception("Should not come here"); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertTrue(e.statusCode == 400 && e.getMessage().equals( + "Http error. Status Code: 400. Message: Field name 'userId' is invalid in JSON input")); + } + } + + { + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "a@a.com", "p1234"); + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("userId", user.getSupertokensUserId()); + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token", requestBody, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + throw new Exception("Should not come here"); + } catch (io.supertokens.test.httpRequest.HttpResponseException e) { + assertTrue(e.statusCode == 400 && e.getMessage().equals( + "Http error. Status Code: 400. Message: Field name 'email' is invalid in JSON input")); + } + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + // Check good input works + @Test + public void testGoodInput() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject signUpResponse = Utils.signUpRequest_2_4(process, "random@gmail.com", "validPass123"); + assertEquals(signUpResponse.get("status").getAsString(), "OK"); + assertEquals(signUpResponse.entrySet().size(), 2); + String userId = signUpResponse.getAsJsonObject("user").get("id").getAsString(); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("userId", userId); + requestBody.addProperty("email", "random@gmail.com"); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token", requestBody, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + + assertEquals(response.get("status").getAsString(), "OK"); + assertNotNull(response.get("token")); + assertEquals(response.entrySet().size(), 2); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testGoodInputWithUserIdMapping() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "a@a.com", "p1234"); + UserIdMapping.createUserIdMapping(process.main, user.getSupertokensUserId(), "e1", null, false); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("userId", "e1"); + requestBody.addProperty("email", "random@gmail.com"); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token", requestBody, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + + assertEquals(response.get("status").getAsString(), "OK"); + assertNotNull(response.get("token")); + assertEquals(response.entrySet().size(), 2); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + // Check for all types of output + // Failure condition: passing a valid userId will cause the test to fail + @Test + public void testForAllTypesOfOutput() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("userId", "randomUserId"); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token", requestBody, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + + assertEquals(response.get("status").getAsString(), "UNKNOWN_USER_ID_ERROR"); + assertEquals(response.entrySet().size(), 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUnknownUserWithUserIdFromNonEp() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + ThirdParty.SignInUpResponse res = ThirdParty.signInUp(process.main, "google", "ug", "t@example.com"); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("userId", res.user.getSupertokensUserId()); + requestBody.addProperty("email", res.user.loginMethods[0].email); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/password/reset/token", requestBody, 1000, 1000, null, + SemVer.v4_0.get(), "emailpassword"); + + assertEquals(response.get("status").getAsString(), "OK"); + assertNotNull(response.get("token")); + assertEquals(response.entrySet().size(), 2); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/emailpassword/api/ImportUserWithPasswordHashAPITest.java b/src/test/java/io/supertokens/test/emailpassword/api/ImportUserWithPasswordHashAPITest.java index 6bf70b186..2422599b5 100644 --- a/src/test/java/io/supertokens/test/emailpassword/api/ImportUserWithPasswordHashAPITest.java +++ b/src/test/java/io/supertokens/test/emailpassword/api/ImportUserWithPasswordHashAPITest.java @@ -22,7 +22,7 @@ import io.supertokens.emailpassword.EmailPassword; import io.supertokens.emailpassword.ParsedFirebaseSCryptResponse; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storageLayer.StorageLayer; @@ -518,7 +518,7 @@ public void testUpdatingAUsersPasswordHash() throws Exception { String email = "test@example.com"; String password = "testPass123"; - UserInfo initialUserInfo = EmailPassword.signUp(process.main, email, password); + AuthRecipeUserInfo initialUserInfo = EmailPassword.signUp(process.main, email, password); // update a user's passwordHash @@ -536,11 +536,11 @@ public void testUpdatingAUsersPasswordHash() throws Exception { assertTrue(response.get("didUserAlreadyExist").getAsBoolean()); // check that a new user was not created by comparing userIds - assertEquals(initialUserInfo.id, response.get("user").getAsJsonObject().get("id").getAsString()); + assertEquals(initialUserInfo.getSupertokensUserId(), response.get("user").getAsJsonObject().get("id").getAsString()); // sign in with the new password to check if the password hash got updated - UserInfo updatedUserInfo = EmailPassword.signIn(process.main, email, newPassword); - assertEquals(updatedUserInfo.passwordHash, passwordHash); + AuthRecipeUserInfo updatedUserInfo = EmailPassword.signIn(process.main, email, newPassword); + assertEquals(updatedUserInfo.loginMethods[0].passwordHash, passwordHash); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -575,9 +575,9 @@ public void testImportingUsersWithHashingAlgorithmFieldWithMixedLowerAndUpperCas assertFalse(response.get("didUserAlreadyExist").getAsBoolean()); // check that the user is created by signing in - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(email, userInfo.email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(email, userInfo.loginMethods[0].email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); } @@ -599,9 +599,9 @@ public void testImportingUsersWithHashingAlgorithmFieldWithMixedLowerAndUpperCas assertFalse(response.get("didUserAlreadyExist").getAsBoolean()); // check that the user is created by signing in - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(email, userInfo.email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(email, userInfo.loginMethods[0].email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); } process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -636,9 +636,9 @@ public void testImportingUsersWithHashingAlgorithmField() throws Exception { assertFalse(response.get("didUserAlreadyExist").getAsBoolean()); // check that the user is created by signing in - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(email, userInfo.email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(email, userInfo.loginMethods[0].email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); } @@ -660,9 +660,9 @@ public void testImportingUsersWithHashingAlgorithmField() throws Exception { assertFalse(response.get("didUserAlreadyExist").getAsBoolean()); // check that the user is created by signing in - UserInfo userInfo = EmailPassword.signIn(process.main, email, password); - assertEquals(email, userInfo.email); - assertEquals(userInfo.passwordHash, passwordHash); + AuthRecipeUserInfo userInfo = EmailPassword.signIn(process.main, email, password); + assertEquals(email, userInfo.loginMethods[0].email); + assertEquals(userInfo.loginMethods[0].passwordHash, passwordHash); } process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/test/emailpassword/api/MultitenantAPITest.java b/src/test/java/io/supertokens/test/emailpassword/api/MultitenantAPITest.java index 6a9627fee..cc4ff6fdf 100644 --- a/src/test/java/io/supertokens/test/emailpassword/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/emailpassword/api/MultitenantAPITest.java @@ -718,22 +718,4 @@ public void testThatTenantIdIsNotAllowedForOlderCDIVersion() throws Exception { assertEquals(404, e.statusCode); } } - - @Test - public void testGetUserByIdForUserThatBelongsToNoTenant() throws Exception { - if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { - return; - } - - createTenants(false); - - { - JsonObject user = TestMultitenancyAPIHelper.epSignUp(t1, "test@example.com", "password", process.getProcess()); - TestMultitenancyAPIHelper.disassociateUserFromTenant(t1, user.get("id").getAsString(), process.getProcess()); - JsonObject userInfoFromId = TestMultitenancyAPIHelper.getEpUserById(t1, user.get("id").getAsString(), - process.getProcess()); - - assertEquals(0, userInfoFromId.get("tenantIds").getAsJsonArray().size()); - } - } } diff --git a/src/test/java/io/supertokens/test/emailpassword/api/SignInAPITest4_0.java b/src/test/java/io/supertokens/test/emailpassword/api/SignInAPITest4_0.java new file mode 100644 index 000000000..c2c081f61 --- /dev/null +++ b/src/test/java/io/supertokens/test/emailpassword/api/SignInAPITest4_0.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.emailpassword.api; + +import com.google.gson.JsonObject; +import io.supertokens.ActiveUsers; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + + +public class SignInAPITest4_0 { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + // Check good input works + @Test + public void testGoodInput() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "random@gmail.com", "validPass123"); + + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "random@gmail.com"); + responseBody.addProperty("password", "validPass123"); + + Thread.sleep(1); // add a small delay to ensure a unique timestamp + long beforeSignIn = System.currentTimeMillis(); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + + JsonObject jsonUser = signInResponse.get("user").getAsJsonObject(); + assert (jsonUser.get("id").getAsString().equals(user.getSupertokensUserId())); + assert (jsonUser.get("timeJoined").getAsLong() == user.timeJoined); + assert (!jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 1); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("random@gmail.com")); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 0); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 1); + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), user.getSupertokensUserId()); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "random@gmail.com"); + assert (lM.entrySet().size() == 6); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), beforeSignIn); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testGoodInputWithUserIdMapping() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "random@gmail.com", "validPass123"); + UserIdMapping.createUserIdMapping(process.main, user.getSupertokensUserId(), "e1", null, false); + + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "random@gmail.com"); + responseBody.addProperty("password", "validPass123"); + + Thread.sleep(1); // add a small delay to ensure a unique timestamp + long beforeSignIn = System.currentTimeMillis(); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + + JsonObject jsonUser = signInResponse.get("user").getAsJsonObject(); + assert (jsonUser.get("id").getAsString().equals("e1")); + assert (jsonUser.get("timeJoined").getAsLong() == user.timeJoined); + assert (!jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 1); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("random@gmail.com")); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 0); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 1); + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), "e1"); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "random@gmail.com"); + assert (lM.entrySet().size() == 6); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), beforeSignIn); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testGoodInputWithUserIdMappingAndMultipleLinkedAccounts() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user0 = EmailPassword.signUp(process.main, "random1@gmail.com", "validPass123"); + UserIdMapping.createUserIdMapping(process.main, user0.getSupertokensUserId(), "e0", null, false); + + Thread.sleep(1); // add a small delay to ensure a unique timestamp + + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "random@gmail.com", "validPass123"); + UserIdMapping.createUserIdMapping(process.main, user.getSupertokensUserId(), "e1", null, false); + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.main, user0.getSupertokensUserId(), user.getSupertokensUserId()); + + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "random@gmail.com"); + responseBody.addProperty("password", "validPass123"); + + Thread.sleep(1); // add a small delay to ensure a unique timestamp + long beforeSignIn = System.currentTimeMillis(); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signin", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + + JsonObject jsonUser = signInResponse.get("user").getAsJsonObject(); + assert (jsonUser.get("id").getAsString().equals("e1")); + assert (jsonUser.get("timeJoined").getAsLong() == user0.timeJoined); + assert (jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 2); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("random@gmail.com") || + jsonUser.get("emails").getAsJsonArray().get(1).getAsString().equals("random@gmail.com")); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("random1@gmail.com") || + jsonUser.get("emails").getAsJsonArray().get(1).getAsString().equals("random1@gmail.com")); + assert (!jsonUser.get("emails").getAsJsonArray().get(0).getAsString() + .equals(jsonUser.get("emails").getAsJsonArray().get(1).getAsString())); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 0); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 2); + { + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(1).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), "e1"); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "random@gmail.com"); + assert (lM.entrySet().size() == 6); + } + { + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertEquals(lM.get("timeJoined").getAsLong(), user0.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), "e0"); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "random1@gmail.com"); + assert (lM.entrySet().size() == 6); + } + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), beforeSignIn); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + +} diff --git a/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest2_7.java b/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest2_7.java index b552b5e5b..7705ee413 100644 --- a/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest2_7.java +++ b/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest2_7.java @@ -17,13 +17,11 @@ package io.supertokens.test.emailpassword.api; import com.google.gson.JsonObject; -import io.supertokens.ActiveUsers; - import io.supertokens.ActiveUsers; import io.supertokens.ProcessState; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; -import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storageLayer.StorageLayer; @@ -147,10 +145,10 @@ public void testGoodInput() throws Exception { int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTS); assert (activeUsers == 1); - UserInfo user = ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getUserInfoUsingEmail(new TenantIdentifier(null, null, null), "random@gmail.com"); - assertEquals(user.email, signUpUser.get("email").getAsString()); - assertEquals(user.id, signUpUser.get("id").getAsString()); + AuthRecipeUserInfo user = ((AuthRecipeStorage) StorageLayer.getStorage(process.getProcess())) + .listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), "random@gmail.com")[0]; + assertEquals(user.loginMethods[0].email, signUpUser.get("email").getAsString()); + assertEquals(user.getSupertokensUserId(), signUpUser.get("id").getAsString()); JsonObject responseBody = new JsonObject(); responseBody.addProperty("email", "random@gmail.com"); @@ -197,10 +195,10 @@ public void testTheNormaliseEmailFunction() throws Exception { assertEquals(signUpUser.get("email").getAsString(), "random@gmail.com"); assertNotNull(signUpUser.get("id")); - UserInfo userInfo = ((EmailPasswordSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getUserInfoUsingId(new AppIdentifier(null, null), signUpUser.get("id").getAsString()); + AuthRecipeUserInfo userInfo = ((AuthRecipeStorage) StorageLayer.getStorage(process.getProcess())) + .getPrimaryUserById(new AppIdentifier(null, null), signUpUser.get("id").getAsString()); - assertEquals(userInfo.email, "random@gmail.com"); + assertEquals(userInfo.loginMethods[0].email, "random@gmail.com"); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest4_0.java b/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest4_0.java new file mode 100644 index 000000000..1dc7cc2ef --- /dev/null +++ b/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest4_0.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.emailpassword.api; + +import com.google.gson.JsonObject; +import io.supertokens.ActiveUsers; +import io.supertokens.ProcessState; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + + +public class SignUpAPITest4_0 { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + // Check good input works + @Test + public void testGoodInput() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "random@gmail.com"); + responseBody.addProperty("password", "validPass123"); + + Thread.sleep(1); // add a small delay to ensure a unique timestamp + long beforeSignIn = System.currentTimeMillis(); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signup", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + + assertEquals(signInResponse.get("status").getAsString(), "OK"); + assertEquals(signInResponse.entrySet().size(), 3); + + JsonObject jsonUser = signInResponse.get("user").getAsJsonObject(); + assertNotNull(jsonUser.get("id")); + assertNotNull(jsonUser.get("timeJoined")); + assert (!jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 1); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("random@gmail.com")); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 0); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 1); + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertNotNull(lM.get("timeJoined")); + assertNotNull(lM.get("recipeUserId")); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "random@gmail.com"); + assert (lM.entrySet().size() == 6); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), beforeSignIn); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + +} diff --git a/src/test/java/io/supertokens/test/emailpassword/api/UserPutAPITest2_8.java b/src/test/java/io/supertokens/test/emailpassword/api/UserPutAPITest2_8.java index c4f62681c..a04258162 100644 --- a/src/test/java/io/supertokens/test/emailpassword/api/UserPutAPITest2_8.java +++ b/src/test/java/io/supertokens/test/emailpassword/api/UserPutAPITest2_8.java @@ -20,7 +20,7 @@ import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -75,12 +75,12 @@ public void testQueryingWithEmailThatAlreadyExists() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); - UserInfo user2 = EmailPassword.signUp(process.getProcess(), "someemail2@gmail.com", "somePass"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "someemail2@gmail.com", "somePass"); JsonObject body = new JsonObject(); - body.addProperty("userId", user.id); - body.addProperty("email", user2.email); + body.addProperty("userId", user.getSupertokensUserId()); + body.addProperty("email", user2.loginMethods[0].email); JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(process.getProcess(), "", "http://localhost:3567/recipe/user", body, 1000, 1000, null, SemVer.v2_8.get(), @@ -98,10 +98,10 @@ public void testUpdatingEmailNormalisesIt() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); JsonObject body = new JsonObject(); - body.addProperty("userId", user.id); + body.addProperty("userId", user.getSupertokensUserId()); body.addProperty("email", "someemail+TEST@gmail.com"); JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(process.getProcess(), "", @@ -144,10 +144,10 @@ public void testSuccessfulUpdate() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); JsonObject body = new JsonObject(); - body.addProperty("userId", user.id); + body.addProperty("userId", user.getSupertokensUserId()); body.addProperty("email", "someOtherEmail@gmail.com"); JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(process.getProcess(), "", @@ -168,10 +168,10 @@ public void testSuccessfulUpdateWithOnlyPassword() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); JsonObject body = new JsonObject(); - body.addProperty("userId", user.id); + body.addProperty("userId", user.getSupertokensUserId()); body.addProperty("password", "somePass123"); JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(process.getProcess(), "", diff --git a/src/test/java/io/supertokens/test/emailpassword/api/UserPutAPITest4_0.java b/src/test/java/io/supertokens/test/emailpassword/api/UserPutAPITest4_0.java new file mode 100644 index 000000000..4754a04b1 --- /dev/null +++ b/src/test/java/io/supertokens/test/emailpassword/api/UserPutAPITest4_0.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.emailpassword.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class UserPutAPITest4_0 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testThatAPIReturnsEmailUpdateNotPossibleWithSingleTenant() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user0 = EmailPassword.signUp(process.getProcess(), "someemail1@gmail.com", "somePass"); + AuthRecipe.createPrimaryUser(process.main, user0.getSupertokensUserId()); + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "someemail@gmail.com", "somePass"); + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + + JsonObject body = new JsonObject(); + body.addProperty("recipeUserId", user.getSupertokensUserId()); + body.addProperty("email", "someemail1@gmail.com"); + + JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user", body, 1000, 1000, null, SemVer.v4_0.get(), + RECIPE_ID.EMAIL_PASSWORD.toString()); + + assertEquals("EMAIL_CHANGE_NOT_ALLOWED_ERROR", response.get("status").getAsString()); + assertEquals("New email is associated with another primary user ID", response.get("reason").getAsString()); + assertEquals(2, response.entrySet().size()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/emailverification/DeleteExpiredEmailVerificationTokensCronjobTest.java b/src/test/java/io/supertokens/test/emailverification/DeleteExpiredEmailVerificationTokensCronjobTest.java index 78beca854..a036ebfae 100644 --- a/src/test/java/io/supertokens/test/emailverification/DeleteExpiredEmailVerificationTokensCronjobTest.java +++ b/src/test/java/io/supertokens/test/emailverification/DeleteExpiredEmailVerificationTokensCronjobTest.java @@ -22,10 +22,9 @@ import io.supertokens.emailpassword.EmailPassword; import io.supertokens.emailverification.EmailVerification; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo; import io.supertokens.pluginInterface.emailverification.sqlStorage.EmailVerificationSQLStorage; -import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; @@ -66,24 +65,24 @@ public void checkingCronJob() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); - String tok = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); - String tok2 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + String tok = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); + String tok2 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); Thread.sleep(2000); - String tok3 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); - String tok4 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + String tok3 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); + String tok4 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); assert (((EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getAllEmailVerificationTokenInfoForUser(new TenantIdentifier(null, null, null), user.id, user.email).length == + .getAllEmailVerificationTokenInfoForUser(new TenantIdentifier(null, null, null), user.getSupertokensUserId(), user.loginMethods[0].email).length == 4); Thread.sleep(3500); EmailVerificationTokenInfo[] tokens = ((EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getAllEmailVerificationTokenInfoForUser(new TenantIdentifier(null, null, null), user.id, user.email); + .getAllEmailVerificationTokenInfoForUser(new TenantIdentifier(null, null, null), user.getSupertokensUserId(), user.loginMethods[0].email); assert (tokens.length == 2); diff --git a/src/test/java/io/supertokens/test/emailverification/EmailVerificationTest.java b/src/test/java/io/supertokens/test/emailverification/EmailVerificationTest.java index de78a89cb..94c9a4238 100644 --- a/src/test/java/io/supertokens/test/emailverification/EmailVerificationTest.java +++ b/src/test/java/io/supertokens/test/emailverification/EmailVerificationTest.java @@ -23,7 +23,7 @@ import io.supertokens.emailverification.exception.EmailAlreadyVerifiedException; import io.supertokens.emailverification.exception.EmailVerificationInvalidTokenException; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailverification.EmailVerificationTokenInfo; import io.supertokens.pluginInterface.emailverification.exception.DuplicateEmailVerificationTokenException; import io.supertokens.pluginInterface.emailverification.sqlStorage.EmailVerificationSQLStorage; @@ -79,14 +79,14 @@ public void testGeneratingEmailVerificationTokenTwoTimes() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); - String token1 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); - String token2 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); + String token1 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); + String token2 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); assertNotEquals(token1, token2); EmailVerificationTokenInfo[] tokenInfo = ((EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess())) - .getAllEmailVerificationTokenInfoForUser(new TenantIdentifier(null, null, null), user.id, user.email); + .getAllEmailVerificationTokenInfoForUser(new TenantIdentifier(null, null, null), user.getSupertokensUserId(), user.loginMethods[0].email); assertEquals(tokenInfo.length, 2); assertTrue((tokenInfo[0].token.equals(io.supertokens.utils.Utils.hashSHA256(token1))) @@ -110,15 +110,15 @@ public void testVerifyingEmailAndGeneratingToken() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); - String token = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); + String token = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); EmailVerification.verifyEmail(process.getProcess(), token); - assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user.id, user.email)); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email)); try { - EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); throw new Exception("should not come here"); } catch (EmailAlreadyVerifiedException ignored) { } @@ -160,13 +160,13 @@ public void testGeneratingTwoTokenVerifyOtherTokenShouldThrowAnError() throws Ex if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); - String token1 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); - String token2 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + String token1 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); + String token2 = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); EmailVerification.verifyEmail(process.getProcess(), token1); - assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user.id, user.email)); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email)); try { EmailVerification.verifyEmail(process.getProcess(), token2); @@ -193,9 +193,9 @@ public void useAnExpiredTokenItShouldThrowAnError() throws Exception { if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); - String token = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + String token = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); Thread.sleep(20); @@ -221,11 +221,11 @@ public void testFormatOfEmailVerificationToken() throws Exception { if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); for (int i = 0; i < 100; i++) { - String verifyToken = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, - user.email); + String verifyToken = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), + user.loginMethods[0].email); assertEquals(verifyToken.length(), 128); assertFalse(verifyToken.contains("+")); assertFalse(verifyToken.contains("=")); @@ -248,11 +248,11 @@ public void clashingEmailVerificationToken() throws Exception { } // we add a user first. - UserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test1@example.com", "password"); ((EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess())) .addEmailVerificationToken(new TenantIdentifier(null, null, null), - new EmailVerificationTokenInfo(user.id, "token", + new EmailVerificationTokenInfo(user.getSupertokensUserId(), "token", System.currentTimeMillis() + Config.getConfig(process.getProcess()).getEmailVerificationTokenLifetime(), "test1@example.com")); @@ -260,7 +260,7 @@ public void clashingEmailVerificationToken() throws Exception { try { ((EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess())) .addEmailVerificationToken(new TenantIdentifier(null, null, null), - new EmailVerificationTokenInfo(user.id, "token", + new EmailVerificationTokenInfo(user.getSupertokensUserId(), "token", System.currentTimeMillis() + Config.getConfig(process.getProcess()).getEmailVerificationTokenLifetime(), @@ -285,17 +285,17 @@ public void verifyEmail() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); - assert (!EmailVerification.isEmailVerified(process.getProcess(), user.id, user.email)); + assert (!EmailVerification.isEmailVerified(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email)); - String token = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + String token = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); assert (token != null); EmailVerification.verifyEmail(process.getProcess(), token); - assert (EmailVerification.isEmailVerified(process.getProcess(), user.id, user.email)); + assert (EmailVerification.isEmailVerified(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -313,24 +313,24 @@ public void testVerifyingEmailAndThenUnverify() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); - String token = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); + String token = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); EmailVerification.verifyEmail(process.getProcess(), token); - assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user.id, user.email)); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email)); ((EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess())).startTransaction(con -> { try { ((EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess())) .updateIsEmailVerified_Transaction(new AppIdentifier(null, null), con, - user.id, user.email, false); + user.getSupertokensUserId(), user.loginMethods[0].email, false); } catch (TenantOrAppNotFoundException e) { throw new RuntimeException(e); } return null; }); - assertFalse(EmailVerification.isEmailVerified(process.getProcess(), user.id, user.email)); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -348,24 +348,24 @@ public void testVerifyingSameEmailTwice() throws Exception { return; } - UserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); - String token = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.id, user.email); + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "testPass123"); + String token = EmailVerification.generateEmailVerificationToken(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email); EmailVerification.verifyEmail(process.getProcess(), token); - assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user.id, user.email)); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email)); ((EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess())).startTransaction(con -> { try { ((EmailVerificationSQLStorage) StorageLayer.getStorage(process.getProcess())) .updateIsEmailVerified_Transaction(new AppIdentifier(null, null), con, - user.id, user.email, true); + user.getSupertokensUserId(), user.loginMethods[0].email, true); } catch (TenantOrAppNotFoundException e) { throw new RuntimeException(e); } return null; }); - assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user.id, user.email)); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user.getSupertokensUserId(), user.loginMethods[0].email)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java b/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java index d7c13d4fd..e1c7f9e0c 100644 --- a/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java +++ b/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java @@ -25,7 +25,7 @@ import io.supertokens.multitenancy.Multitenancy; import io.supertokens.pluginInterface.ActiveUsersStorage; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; import io.supertokens.storageLayer.StorageLayer; @@ -120,9 +120,9 @@ public void testDeletingAppDeleteNonAuthRecipeData() throws Exception { StorageLayer.getStorage(t, process.getProcess())); - UserInfo user = EmailPassword.signUp(tWithStorage, process.getProcess(), "test@example.com", + AuthRecipeUserInfo user = EmailPassword.signUp(tWithStorage, process.getProcess(), "test@example.com", "password"); - String userId = user.id; + String userId = user.getSupertokensUserId(); // create entry in nonAuth table StorageLayer.getStorage(process.main).addInfoToNonAuthRecipesBasedOnUserId(app, className, userId); @@ -223,8 +223,8 @@ public void testDisassociationOfUserDeletesNonAuthRecipeData() throws Exception continue; } - UserInfo user = EmailPassword.signUp(appWithStorage, process.getProcess(), "test@example.com", "password"); - String userId = user.id; + AuthRecipeUserInfo user = EmailPassword.signUp(appWithStorage, process.getProcess(), "test@example.com", "password"); + String userId = user.getSupertokensUserId(); Multitenancy.addUserIdToTenant(process.getProcess(), tenantWithStorage, userId); @@ -291,18 +291,18 @@ public void deletingTenantKeepsTheUserInTheApp() throws Exception { TenantIdentifierWithStorage tenantWithStorage = tenant.withStorage( StorageLayer.getStorage(tenant, process.getProcess())); - UserInfo user = EmailPassword.signUp(tenantWithStorage, process.getProcess(), "test@example.com", "password"); - String userId = user.id; + AuthRecipeUserInfo user = EmailPassword.signUp(tenantWithStorage, process.getProcess(), "test@example.com", "password"); + String userId = user.getSupertokensUserId(); Multitenancy.deleteTenant(tenant, process.getProcess()); Multitenancy.addUserIdToTenant(process.getProcess(), appWithStorage, userId); // user id must be intact to do this - UserInfo appUser = EmailPassword.getUserUsingId(appWithStorage.toAppIdentifierWithStorage(), userId); + AuthRecipeUserInfo appUser = EmailPassword.getUserUsingId(appWithStorage.toAppIdentifierWithStorage(), userId); assertNotNull(appUser); - assertEquals(userId, appUser.id); + assertEquals(userId, appUser.getSupertokensUserId()); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/test/multitenant/TestAppData.java b/src/test/java/io/supertokens/test/multitenant/TestAppData.java index 9bee53052..f6c4c679e 100644 --- a/src/test/java/io/supertokens/test/multitenant/TestAppData.java +++ b/src/test/java/io/supertokens/test/multitenant/TestAppData.java @@ -29,7 +29,7 @@ import io.supertokens.multitenancy.Multitenancy; import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.totp.TOTPDevice; @@ -94,7 +94,8 @@ public void testThatDeletingAppDeleteDataFromAllTables() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); FeatureFlagTestContent.getInstance(process.getProcess()) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY, EE_FEATURES.TOTP}); + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, + new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY, EE_FEATURES.TOTP}); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -114,17 +115,19 @@ public void testThatDeletingAppDeleteDataFromAllTables() throws Exception { new JsonObject() ), false); - TenantIdentifierWithStorage appWithStorage = app.withStorage(StorageLayer.getStorage(app, process.getProcess())); + TenantIdentifierWithStorage appWithStorage = app.withStorage( + StorageLayer.getStorage(app, process.getProcess())); String[] allTableNames = appWithStorage.getStorage().getAllTablesInTheDatabase(); allTableNames = removeStrings(allTableNames, tablesToIgnore); Arrays.sort(allTableNames); // Add all recipe data - UserInfo epUser = EmailPassword.signUp(appWithStorage, process.getProcess(), "test@example.com", "password"); - EmailPassword.generatePasswordResetToken(appWithStorage, process.getProcess(), epUser.id); + AuthRecipeUserInfo epUser = EmailPassword.signUp(appWithStorage, process.getProcess(), "test@example.com", "password"); + EmailPassword.generatePasswordResetTokenBeforeCdi4_0(appWithStorage, process.getProcess(), epUser.getSupertokensUserId()); - ThirdParty.SignInUpResponse tpUser = ThirdParty.signInUp(appWithStorage, process.getProcess(), "google", "googleid", "test@example.com"); + ThirdParty.SignInUpResponse tpUser = ThirdParty.signInUp(appWithStorage, process.getProcess(), "google", + "googleid", "test@example.com"); Passwordless.CreateCodeResponse code = Passwordless.createCode(appWithStorage, process.getProcess(), "test@example.com", null, null, null); @@ -132,28 +135,37 @@ public void testThatDeletingAppDeleteDataFromAllTables() throws Exception { code.deviceId, code.deviceIdHash, code.userInputCode, null); Passwordless.createCode(appWithStorage, process.getProcess(), "test@example.com", null, null, null); - Dashboard.signUpDashboardUser(appWithStorage.toAppIdentifierWithStorage(), process.getProcess(), "user@example.com", "password"); - Dashboard.signInDashboardUser(appWithStorage.toAppIdentifierWithStorage(), process.getProcess(), "user@example.com", "password"); + Dashboard.signUpDashboardUser(appWithStorage.toAppIdentifierWithStorage(), process.getProcess(), + "user@example.com", "password"); + Dashboard.signInDashboardUser(appWithStorage.toAppIdentifierWithStorage(), process.getProcess(), + "user@example.com", "password"); - String evToken = EmailVerification.generateEmailVerificationToken(appWithStorage, process.getProcess(), epUser.id, epUser.email); + String evToken = EmailVerification.generateEmailVerificationToken(appWithStorage, process.getProcess(), + epUser.getSupertokensUserId(), epUser.loginMethods[0].email); EmailVerification.verifyEmail(appWithStorage, evToken); - EmailVerification.generateEmailVerificationToken(appWithStorage, process.getProcess(), tpUser.user.id, tpUser.user.email); + EmailVerification.generateEmailVerificationToken(appWithStorage, process.getProcess(), tpUser.user.getSupertokensUserId(), + tpUser.user.loginMethods[0].email); - Session.createNewSession(appWithStorage, process.getProcess(), epUser.id, new JsonObject(), new JsonObject()); + Session.createNewSession(appWithStorage, process.getProcess(), epUser.getSupertokensUserId(), new JsonObject(), new JsonObject()); - UserRoles.createNewRoleOrModifyItsPermissions(appWithStorage.toAppIdentifierWithStorage(), "role", new String[]{"permission1", "permission2"}); - UserRoles.addRoleToUser(appWithStorage, epUser.id, "role"); + UserRoles.createNewRoleOrModifyItsPermissions(appWithStorage.toAppIdentifierWithStorage(), "role", + new String[]{"permission1", "permission2"}); + UserRoles.addRoleToUser(appWithStorage, epUser.getSupertokensUserId(), "role"); - TOTPDevice totpDevice = Totp.registerDevice(appWithStorage.toAppIdentifierWithStorage(), process.getProcess(), epUser.id, "test", 1, 3); - Totp.verifyCode(appWithStorage, process.getProcess(), epUser.id, generateTotpCode(process.getProcess(), totpDevice, 0), true); + TOTPDevice totpDevice = Totp.registerDevice(appWithStorage.toAppIdentifierWithStorage(), process.getProcess(), + epUser.getSupertokensUserId(), "test", 1, 3); + Totp.verifyCode(appWithStorage, process.getProcess(), epUser.getSupertokensUserId(), + generateTotpCode(process.getProcess(), totpDevice, 0), true); - ActiveUsers.updateLastActive(appWithStorage.toAppIdentifierWithStorage(), process.getProcess(), epUser.id); + ActiveUsers.updateLastActive(appWithStorage.toAppIdentifierWithStorage(), process.getProcess(), epUser.getSupertokensUserId()); - UserMetadata.updateUserMetadata(appWithStorage.toAppIdentifierWithStorage(), epUser.id, new JsonObject()); + UserMetadata.updateUserMetadata(appWithStorage.toAppIdentifierWithStorage(), epUser.getSupertokensUserId(), new JsonObject()); - UserIdMapping.createUserIdMapping(process.getProcess(), appWithStorage.toAppIdentifierWithStorage(), plUser.user.id, "externalid", null, false); + UserIdMapping.createUserIdMapping(process.getProcess(), appWithStorage.toAppIdentifierWithStorage(), + plUser.user.getSupertokensUserId(), "externalid", null, false); - String[] tablesThatHaveData = appWithStorage.getStorage().getAllTablesInTheDatabaseThatHasDataForAppId(app.getAppId()); + String[] tablesThatHaveData = appWithStorage.getStorage() + .getAllTablesInTheDatabaseThatHasDataForAppId(app.getAppId()); tablesThatHaveData = removeStrings(tablesThatHaveData, tablesToIgnore); Arrays.sort(tablesThatHaveData); @@ -166,7 +178,7 @@ public void testThatDeletingAppDeleteDataFromAllTables() throws Exception { tablesThatHaveData = appWithStorage.getStorage().getAllTablesInTheDatabaseThatHasDataForAppId(app.getAppId()); tablesThatHaveData = removeStrings(tablesThatHaveData, tablesToIgnore); assertEquals(0, tablesThatHaveData.length); - + process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java b/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java index 8d5ff811c..261a0eb27 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java @@ -24,10 +24,12 @@ import io.supertokens.test.httpRequest.HttpRequestForTesting; import io.supertokens.test.httpRequest.HttpResponseException; import io.supertokens.utils.SemVer; +import io.supertokens.webserver.WebserverAPI; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Random; import static org.junit.Assert.assertEquals; @@ -201,12 +203,12 @@ public static JsonObject getTenant(TenantIdentifier tenantIdentifier, Main main) public static JsonObject associateUserToTenant(TenantIdentifier tenantIdentifier, String userId, Main main) throws HttpResponseException, IOException { JsonObject requestBody = new JsonObject(); - requestBody.addProperty("userId", userId); + requestBody.addProperty("recipeUserId", userId); JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/multitenancy/tenant/user"), requestBody, 1000, 1000, null, - SemVer.v3_0.get(), "multitenancy"); + WebserverAPI.getLatestCDIVersion().get(), "multitenancy"); return response; } @@ -214,12 +216,12 @@ public static JsonObject associateUserToTenant(TenantIdentifier tenantIdentifier public static JsonObject disassociateUserFromTenant(TenantIdentifier tenantIdentifier, String userId, Main main) throws HttpResponseException, IOException { JsonObject requestBody = new JsonObject(); - requestBody.addProperty("userId", userId); + requestBody.addProperty("recipeUserId", userId); JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/multitenancy/tenant/user/remove"), requestBody, 1000, 1000, null, - SemVer.v3_0.get(), "multitenancy"); + WebserverAPI.getLatestCDIVersion().get(), "multitenancy"); assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); return response; @@ -315,6 +317,80 @@ public static JsonObject tpSignInUp(TenantIdentifier tenantIdentifier, String th return response.get("user").getAsJsonObject(); } + private static String generateRandomString(int length) { + StringBuilder sb = new StringBuilder(length); + final String ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + final Random RANDOM = new Random(); + for (int i = 0; i < length; i++) { + int randomIndex = RANDOM.nextInt(ALPHABET.length()); + char randomChar = ALPHABET.charAt(randomIndex); + sb.append(randomChar); + } + return sb.toString(); + } + + private static JsonObject createCodeWithEmail(TenantIdentifier tenantIdentifier, String email, Main main) + throws HttpResponseException, IOException { + String exampleCode = generateRandomString(6); + JsonObject createCodeRequestBody = new JsonObject(); + createCodeRequestBody.addProperty("email", email); + createCodeRequestBody.addProperty("userInputCode", exampleCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup/code"), + createCodeRequestBody, 1000, 1000, null, + SemVer.v3_0.get(), "passwordless"); + + assertEquals("OK", response.get("status").getAsString()); + assertEquals(8, response.entrySet().size()); + + return response; + } + + private static JsonObject consumeCode(TenantIdentifier tenantIdentifier, String deviceId, String preAuthSessionId, + String userInputCode, Main main) + throws HttpResponseException, IOException { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", preAuthSessionId); + consumeCodeRequestBody.addProperty("userInputCode", userInputCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup/code/consume"), + consumeCodeRequestBody, 1000, 1000, null, + SemVer.v3_0.get(), "passwordless"); + assertEquals("OK", response.get("status").getAsString()); + return response.get("user").getAsJsonObject(); + } + + public static JsonObject plSignInUpEmail(TenantIdentifier tenantIdentifier, String email, Main main) + throws HttpResponseException, IOException { + JsonObject code = createCodeWithEmail(tenantIdentifier, email, main); + return consumeCode(tenantIdentifier, code.get("deviceId").getAsString(), code.get("preAuthSessionId").getAsString(), code.get("userInputCode").getAsString(), main); + } + + private static JsonObject createCodeWithNumber(TenantIdentifier tenantIdentifier, String phoneNumber, Main main) + throws HttpResponseException, IOException { + JsonObject createCodeRequestBody = new JsonObject(); + createCodeRequestBody.addProperty("phoneNumber", phoneNumber); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup/code"), + createCodeRequestBody, 1000, 1000, null, + SemVer.v3_0.get(), "passwordless"); + + assertEquals("OK", response.get("status").getAsString()); + assertEquals(8, response.entrySet().size()); + + return response; + } + + public static JsonObject plSignInUpNumber(TenantIdentifier tenantIdentifier, String phoneNumber, Main main) + throws HttpResponseException, IOException { + JsonObject code = createCodeWithNumber(tenantIdentifier, phoneNumber, main); + return consumeCode(tenantIdentifier, code.get("deviceId").getAsString(), code.get("preAuthSessionId").getAsString(), code.get("userInputCode").getAsString(), main); + } + public static void addLicense(String licenseKey, Main main) throws HttpResponseException, IOException { JsonObject licenseKeyRequest = new JsonObject(); licenseKeyRequest.addProperty("licenseKey", licenseKey); diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestTenantIdIsNotPresentForOlderCDI.java b/src/test/java/io/supertokens/test/multitenant/api/TestTenantIdIsNotPresentForOlderCDI.java index 3bfc81caf..1b12646d4 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestTenantIdIsNotPresentForOlderCDI.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestTenantIdIsNotPresentForOlderCDI.java @@ -22,6 +22,7 @@ import io.supertokens.Main; import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; @@ -31,7 +32,7 @@ import io.supertokens.passwordless.Passwordless; import io.supertokens.passwordless.exceptions.*; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -270,7 +271,8 @@ private void createUsers(TenantIdentifier tenantIdentifier, int numUsers, String throws TenantOrAppNotFoundException, DuplicateEmailException, StorageQueryException, BadPermissionException, DuplicateLinkCodeHashException, NoSuchAlgorithmException, IOException, RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, - StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException, + EmailChangeNotAllowedException { HashMap> tenantToUsers = new HashMap<>(); HashMap> recipeToUsers = new HashMap<>(); @@ -279,17 +281,18 @@ private void createUsers(TenantIdentifier tenantIdentifier, int numUsers, String tenantToUsers.put(tenantIdentifier, new ArrayList<>()); } - TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, process.getProcess())); + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage( + StorageLayer.getStorage(tenantIdentifier, process.getProcess())); for (int i = 0; i < numUsers; i++) { { - UserInfo user = EmailPassword.signUp( + AuthRecipeUserInfo user = EmailPassword.signUp( tenantIdentifierWithStorage, process.getProcess(), prefix + "epuser" + i + "@example.com", "password" + i); - tenantToUsers.get(tenantIdentifier).add(user.id); + tenantToUsers.get(tenantIdentifier).add(user.getSupertokensUserId()); if (!recipeToUsers.containsKey("emailpassword")) { recipeToUsers.put("emailpassword", new ArrayList<>()); } - recipeToUsers.get("emailpassword").add(user.id); + recipeToUsers.get("emailpassword").add(user.getSupertokensUserId()); } { Passwordless.CreateCodeResponse codeResponse = Passwordless.createCode( @@ -302,32 +305,33 @@ private void createUsers(TenantIdentifier tenantIdentifier, int numUsers, String Passwordless.ConsumeCodeResponse response = Passwordless.consumeCode(tenantIdentifierWithStorage, process.getProcess(), codeResponse.deviceId, codeResponse.deviceIdHash, "abcd", null); - tenantToUsers.get(tenantIdentifier).add(response.user.id); + tenantToUsers.get(tenantIdentifier).add(response.user.getSupertokensUserId()); if (!recipeToUsers.containsKey("passwordless")) { recipeToUsers.put("passwordless", new ArrayList<>()); } - recipeToUsers.get("passwordless").add(response.user.id); + recipeToUsers.get("passwordless").add(response.user.getSupertokensUserId()); } { ThirdParty.SignInUpResponse user1 = ThirdParty.signInUp(tenantIdentifierWithStorage, process.getProcess(), "google", "googleid" + i, prefix + "tpuser" + i + "@example.com"); - tenantToUsers.get(tenantIdentifier).add(user1.user.id); + tenantToUsers.get(tenantIdentifier).add(user1.user.getSupertokensUserId()); ThirdParty.SignInUpResponse user2 = ThirdParty.signInUp(tenantIdentifierWithStorage, process.getProcess(), "facebook", "fbid" + i, prefix + "tpuser" + i + "@example.com"); - tenantToUsers.get(tenantIdentifier).add(user2.user.id); + tenantToUsers.get(tenantIdentifier).add(user2.user.getSupertokensUserId()); if (!recipeToUsers.containsKey("thirdparty")) { recipeToUsers.put("thirdparty", new ArrayList<>()); } - recipeToUsers.get("thirdparty").add(user1.user.id); - recipeToUsers.get("thirdparty").add(user2.user.id); + recipeToUsers.get("thirdparty").add(user1.user.getSupertokensUserId()); + recipeToUsers.get("thirdparty").add(user2.user.getSupertokensUserId()); } } } - public static JsonObject listUsers(TenantIdentifier sourceTenant, String paginationToken, String limit, String includeRecipeIds, Main main) + public static JsonObject listUsers(TenantIdentifier sourceTenant, String paginationToken, String limit, + String includeRecipeIds, Main main) throws HttpResponseException, IOException { Map params = new HashMap<>(); if (paginationToken != null) { @@ -396,7 +400,8 @@ public void testUserPaginationUserObjectsDontHaveTenantIdsInOlderCDIVersion() th } { // recipe combinations - String[] combinations = new String[]{"emailpassword", "passwordless", "thirdparty", "emailpassword,passwordless", "emailpassword,thirdparty", "passwordless,thirdparty"}; + String[] combinations = new String[]{"emailpassword", "passwordless", "thirdparty", + "emailpassword,passwordless", "emailpassword,thirdparty", "passwordless,thirdparty"}; int[] userCounts = new int[]{50, 50, 100, 100, 150, 150}; for (int i = 0; i < combinations.length; i++) { @@ -405,7 +410,8 @@ public void testUserPaginationUserObjectsDontHaveTenantIdsInOlderCDIVersion() th Set userIdSet = new HashSet<>(); - JsonObject userList = listUsers(tenantIdentifier, null, "10", includeRecipeIds, process.getProcess()); + JsonObject userList = listUsers(tenantIdentifier, null, "10", includeRecipeIds, + process.getProcess()); String paginationToken = userList.get("nextPaginationToken").getAsString(); JsonArray users = userList.get("users").getAsJsonArray(); @@ -419,11 +425,13 @@ public void testUserPaginationUserObjectsDontHaveTenantIdsInOlderCDIVersion() th } while (paginationToken != null) { - userList = listUsers(tenantIdentifier, paginationToken, "10", includeRecipeIds, process.getProcess()); + userList = listUsers(tenantIdentifier, paginationToken, "10", includeRecipeIds, + process.getProcess()); users = userList.get("users").getAsJsonArray(); for (JsonElement user : users) { - String userId = user.getAsJsonObject().get("user").getAsJsonObject().get("id").getAsString(); + String userId = user.getAsJsonObject().get("user").getAsJsonObject().get("id") + .getAsString(); String recipeId = user.getAsJsonObject().get("recipeId").getAsString(); assertFalse(userIdSet.contains(userId)); userIdSet.add(userId); @@ -504,7 +512,8 @@ private JsonObject consumeCode(TenantIdentifier tenantIdentifier, String preAuth return response.get("user").getAsJsonObject(); } - private JsonObject consumeCode(TenantIdentifier tenantIdentifier, String deviceId, String preAuthSessionId, String userInputCode) + private JsonObject consumeCode(TenantIdentifier tenantIdentifier, String deviceId, String preAuthSessionId, + String userInputCode) throws HttpResponseException, IOException { JsonObject consumeCodeRequestBody = new JsonObject(); consumeCodeRequestBody.addProperty("deviceId", deviceId); @@ -522,25 +531,29 @@ private JsonObject consumeCode(TenantIdentifier tenantIdentifier, String deviceI private JsonObject signInUpEmailUsingLinkCode(TenantIdentifier tenantIdentifier, String email) throws HttpResponseException, IOException { JsonObject code = createCodeWithEmail(tenantIdentifier, email); - return consumeCode(tenantIdentifier, code.get("preAuthSessionId").getAsString(), code.get("linkCode").getAsString()); + return consumeCode(tenantIdentifier, code.get("preAuthSessionId").getAsString(), + code.get("linkCode").getAsString()); } private JsonObject signInUpEmailUsingUserInputCode(TenantIdentifier tenantIdentifier, String email) throws HttpResponseException, IOException { JsonObject code = createCodeWithEmail(tenantIdentifier, email); - return consumeCode(tenantIdentifier, code.get("deviceId").getAsString(), code.get("preAuthSessionId").getAsString(), code.get("userInputCode").getAsString()); + return consumeCode(tenantIdentifier, code.get("deviceId").getAsString(), + code.get("preAuthSessionId").getAsString(), code.get("userInputCode").getAsString()); } private JsonObject signInUpNumberUsingLinkCode(TenantIdentifier tenantIdentifier, String phoneNumber) throws HttpResponseException, IOException { JsonObject code = createCodeWithNumber(tenantIdentifier, phoneNumber); - return consumeCode(tenantIdentifier, code.get("preAuthSessionId").getAsString(), code.get("linkCode").getAsString()); + return consumeCode(tenantIdentifier, code.get("preAuthSessionId").getAsString(), + code.get("linkCode").getAsString()); } private JsonObject signInUpNumberUsingUserInputCode(TenantIdentifier tenantIdentifier, String phoneNumber) throws HttpResponseException, IOException { JsonObject code = createCodeWithNumber(tenantIdentifier, phoneNumber); - return consumeCode(tenantIdentifier, code.get("deviceId").getAsString(), code.get("preAuthSessionId").getAsString(), code.get("userInputCode").getAsString()); + return consumeCode(tenantIdentifier, code.get("deviceId").getAsString(), + code.get("preAuthSessionId").getAsString(), code.get("userInputCode").getAsString()); } private JsonObject plessGetUserUsingId(TenantIdentifier tenantIdentifier, String userId) @@ -622,7 +635,8 @@ public void testPlessUsersDontHaveTenantIdsInOlderCDI() throws Exception { } } - public JsonObject signInUp(TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId, String email) + public JsonObject signInUp(TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId, + String email) throws HttpResponseException, IOException { JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", email); @@ -654,7 +668,8 @@ private JsonObject tpGetUserUsingId(TenantIdentifier tenantIdentifier, String us return userResponse.getAsJsonObject("user"); } - private JsonObject getUserUsingThirdPartyUserId(TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId) + private JsonObject getUserUsingThirdPartyUserId(TenantIdentifier tenantIdentifier, String thirdPartyId, + String thirdPartyUserId) throws HttpResponseException, IOException { HashMap map = new HashMap<>(); map.put("thirdPartyId", thirdPartyId); @@ -694,7 +709,7 @@ public void testTpUsersDontHaveTenantIdsForOlderCDI() throws Exception { return; } - for (TenantIdentifier t: new TenantIdentifier[]{t1, t2, t3}) { + for (TenantIdentifier t : new TenantIdentifier[]{t1, t2, t3}) { JsonObject user1 = signInUp(t, "google", "google-user-id", "user@gmail.com"); JsonObject user2 = signInUp(t, "facebook", "fb-user-id", "user@gmail.com"); diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java b/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java index 568c22a9b..9c86c127e 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java @@ -29,7 +29,7 @@ import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.ActiveUsersStorage; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.jwt.JWTRecipeStorage; @@ -278,21 +278,21 @@ public void testEmailPasswordUsersHaveTenantIds() throws Exception { TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); - UserInfo user = EmailPassword.signUp(t1WithStorage, + AuthRecipeUserInfo user = EmailPassword.signUp(t1WithStorage, process.getProcess(), "user@example.com", "password"); - assertArrayEquals(new String[]{"t1"}, user.tenantIds); + assertArrayEquals(new String[]{"t1"}, user.tenantIds.toArray()); - Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user.id); - user = EmailPassword.getUserUsingId(t1WithStorage.toAppIdentifierWithStorage(), user.id); - Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds); + Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user.getSupertokensUserId()); + user = EmailPassword.getUserUsingId(t1WithStorage.toAppIdentifierWithStorage(), user.getSupertokensUserId()); + Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds.toArray()); - user = EmailPassword.getUserUsingEmail(t1WithStorage, user.email); - Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds); + user = EmailPassword.getUserUsingEmail(t1WithStorage, user.loginMethods[0].email); + Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds.toArray()); - Multitenancy.removeUserIdFromTenant(process.getProcess(), t1WithStorage, user.id, null); - user = EmailPassword.getUserUsingId(t1WithStorage.toAppIdentifierWithStorage(), user.id); - assertArrayEquals(new String[]{"t2"}, user.tenantIds); + Multitenancy.removeUserIdFromTenant(process.getProcess(), t1WithStorage, user.getSupertokensUserId(), null); + user = EmailPassword.getUserUsingId(t1WithStorage.toAppIdentifierWithStorage(), user.getSupertokensUserId()); + assertArrayEquals(new String[]{"t2"}, user.tenantIds.toArray()); } @Test @@ -314,19 +314,19 @@ public void testPasswordlessUsersHaveTenantIds1() throws Exception { Passwordless.ConsumeCodeResponse consumeCodeResponse = Passwordless.consumeCode(t1WithStorage, process.getProcess(), createCodeResponse.deviceId, createCodeResponse.deviceIdHash, createCodeResponse.userInputCode, null); - assertArrayEquals(new String[]{"t1"}, consumeCodeResponse.user.tenantIds); + assertArrayEquals(new String[]{"t1"}, consumeCodeResponse.user.tenantIds.toArray()); - io.supertokens.pluginInterface.passwordless.UserInfo user; - Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, consumeCodeResponse.user.id); - user = Passwordless.getUserById(t1WithStorage.toAppIdentifierWithStorage(), consumeCodeResponse.user.id); - Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds); + AuthRecipeUserInfo user; + Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, consumeCodeResponse.user.getSupertokensUserId()); + user = Passwordless.getUserById(t1WithStorage.toAppIdentifierWithStorage(), consumeCodeResponse.user.getSupertokensUserId()); + Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds.toArray()); - user = Passwordless.getUserByEmail(t1WithStorage, consumeCodeResponse.user.email); - Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds); + user = Passwordless.getUserByEmail(t1WithStorage, consumeCodeResponse.user.loginMethods[0].email); + Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds.toArray()); - Multitenancy.removeUserIdFromTenant(process.getProcess(), t1WithStorage, consumeCodeResponse.user.id, null); - user = Passwordless.getUserById(t1WithStorage.toAppIdentifierWithStorage(), consumeCodeResponse.user.id); - assertArrayEquals(new String[]{"t2"}, user.tenantIds); + Multitenancy.removeUserIdFromTenant(process.getProcess(), t1WithStorage, consumeCodeResponse.user.getSupertokensUserId(), null); + user = Passwordless.getUserById(t1WithStorage.toAppIdentifierWithStorage(), consumeCodeResponse.user.getSupertokensUserId()); + assertArrayEquals(new String[]{"t2"}, user.tenantIds.toArray()); } @Test @@ -348,19 +348,19 @@ public void testPasswordlessUsersHaveTenantIds2() throws Exception { Passwordless.ConsumeCodeResponse consumeCodeResponse = Passwordless.consumeCode(t1WithStorage, process.getProcess(), createCodeResponse.deviceId, createCodeResponse.deviceIdHash, createCodeResponse.userInputCode, null); - assertArrayEquals(new String[]{"t1"}, consumeCodeResponse.user.tenantIds); + assertArrayEquals(new String[]{"t1"}, consumeCodeResponse.user.tenantIds.toArray()); - io.supertokens.pluginInterface.passwordless.UserInfo user; - Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, consumeCodeResponse.user.id); - user = Passwordless.getUserById(t1WithStorage.toAppIdentifierWithStorage(), consumeCodeResponse.user.id); - Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds); + AuthRecipeUserInfo user; + Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, consumeCodeResponse.user.getSupertokensUserId()); + user = Passwordless.getUserById(t1WithStorage.toAppIdentifierWithStorage(), consumeCodeResponse.user.getSupertokensUserId()); + Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds.toArray()); - user = Passwordless.getUserByPhoneNumber(t1WithStorage, consumeCodeResponse.user.phoneNumber); - Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds); + user = Passwordless.getUserByPhoneNumber(t1WithStorage, consumeCodeResponse.user.loginMethods[0].phoneNumber); + Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds.toArray()); - Multitenancy.removeUserIdFromTenant(process.getProcess(), t1WithStorage, consumeCodeResponse.user.id, null); - user = Passwordless.getUserById(t1WithStorage.toAppIdentifierWithStorage(), consumeCodeResponse.user.id); - assertArrayEquals(new String[]{"t2"}, user.tenantIds); + Multitenancy.removeUserIdFromTenant(process.getProcess(), t1WithStorage, consumeCodeResponse.user.getSupertokensUserId(), null); + user = Passwordless.getUserById(t1WithStorage.toAppIdentifierWithStorage(), consumeCodeResponse.user.getSupertokensUserId()); + assertArrayEquals(new String[]{"t2"}, user.tenantIds.toArray()); } @Test @@ -380,25 +380,25 @@ public void testThirdPartyUsersHaveTenantIds() throws Exception { ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(t1WithStorage, process.getProcess(), "google", "googleid", "user@example.com"); - assertArrayEquals(new String[]{"t1"}, signInUpResponse.user.tenantIds); + assertArrayEquals(new String[]{"t1"}, signInUpResponse.user.tenantIds.toArray()); - Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, signInUpResponse.user.id); - io.supertokens.pluginInterface.thirdparty.UserInfo user = ThirdParty.getUser( - t1WithStorage.toAppIdentifierWithStorage(), signInUpResponse.user.id); - Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds); + Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, signInUpResponse.user.getSupertokensUserId()); + AuthRecipeUserInfo user = ThirdParty.getUser( + t1WithStorage.toAppIdentifierWithStorage(), signInUpResponse.user.getSupertokensUserId()); + Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds.toArray()); - user = ThirdParty.getUsersByEmail(t1WithStorage, signInUpResponse.user.email)[0]; - Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds); + user = ThirdParty.getUsersByEmail(t1WithStorage, signInUpResponse.user.loginMethods[0].email)[0]; + Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds.toArray()); user = ThirdParty.getUser(t1WithStorage, "google", "googleid"); - Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds); + Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds.toArray()); user = ThirdParty.getUser(t2WithStorage, "google", "googleid"); - Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds); + Utils.assertArrayEqualsIgnoreOrder(new String[]{"t1", "t2"}, user.tenantIds.toArray()); - Multitenancy.removeUserIdFromTenant(process.getProcess(), t1WithStorage, signInUpResponse.user.id, null); - user = ThirdParty.getUser(t1WithStorage.toAppIdentifierWithStorage(), signInUpResponse.user.id); - assertArrayEquals(new String[]{"t2"}, user.tenantIds); + Multitenancy.removeUserIdFromTenant(process.getProcess(), t1WithStorage, signInUpResponse.user.getSupertokensUserId(), null); + user = ThirdParty.getUser(t1WithStorage.toAppIdentifierWithStorage(), signInUpResponse.user.getSupertokensUserId()); + assertArrayEquals(new String[]{"t2"}, user.tenantIds.toArray()); } @Test @@ -501,4 +501,76 @@ public void testDisassociateUserWithUserIdMappingAndSession() throws Exception { // OK } } + + @Test + public void testThatUserWithSameEmailCannotBeAssociatedToATenantForEp() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(); + JsonObject user1 = TestMultitenancyAPIHelper.epSignUp(new TenantIdentifier(null, "a1", "t1"), "user@example.com", + "password", process.getProcess()); + String userId1 = user1.get("id").getAsString(); + + TestMultitenancyAPIHelper.epSignUp(new TenantIdentifier(null, "a1", "t2"), "user@example.com", + "password", process.getProcess()); + + JsonObject response = TestMultitenancyAPIHelper.associateUserToTenant(new TenantIdentifier(null, "a1", "t2"), userId1, process.getProcess()); + assertEquals("EMAIL_ALREADY_EXISTS_ERROR", response.getAsJsonPrimitive("status").getAsString()); + } + + @Test + public void testThatUserWithSameThirdPartyInfoCannotBeAssociatedToATenantForTp() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(); + JsonObject user1 = TestMultitenancyAPIHelper.tpSignInUp(new TenantIdentifier(null, "a1", "t1"), "google", "google-user", "user@example.com", + process.getProcess()); + String userId1 = user1.get("id").getAsString(); + + TestMultitenancyAPIHelper.tpSignInUp(new TenantIdentifier(null, "a1", "t2"), "google", "google-user", "user@example.com", + process.getProcess()); + + JsonObject response = TestMultitenancyAPIHelper.associateUserToTenant(new TenantIdentifier(null, "a1", "t2"), userId1, process.getProcess()); + assertEquals("THIRD_PARTY_USER_ALREADY_EXISTS_ERROR", response.getAsJsonPrimitive("status").getAsString()); + } + + @Test + public void testThatUserWithSameEmailCannotBeAssociatedToATenantForPless() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(); + JsonObject user1 = TestMultitenancyAPIHelper.plSignInUpEmail(new TenantIdentifier(null, "a1", "t1"), "user@example.com", + process.getProcess()); + String userId1 = user1.get("id").getAsString(); + + TestMultitenancyAPIHelper.plSignInUpEmail(new TenantIdentifier(null, "a1", "t2"), "user@example.com", + process.getProcess()); + + JsonObject response = TestMultitenancyAPIHelper.associateUserToTenant(new TenantIdentifier(null, "a1", "t2"), userId1, process.getProcess()); + assertEquals("EMAIL_ALREADY_EXISTS_ERROR", response.getAsJsonPrimitive("status").getAsString()); + } + + @Test + public void testThatUserWithSamePhoneCannotBeAssociatedToATenantForPless() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + createTenants(); + JsonObject user1 = TestMultitenancyAPIHelper.plSignInUpNumber(new TenantIdentifier(null, "a1", "t1"), "+919876543210", + process.getProcess()); + String userId1 = user1.get("id").getAsString(); + + TestMultitenancyAPIHelper.plSignInUpNumber(new TenantIdentifier(null, "a1", "t2"), "+919876543210", + process.getProcess()); + + JsonObject response = TestMultitenancyAPIHelper.associateUserToTenant(new TenantIdentifier(null, "a1", "t2"), userId1, process.getProcess()); + assertEquals("PHONE_NUMBER_ALREADY_EXISTS_ERROR", response.getAsJsonPrimitive("status").getAsString()); + } } diff --git a/src/test/java/io/supertokens/test/passwordless/PasswordlessConsumeCodeTest.java b/src/test/java/io/supertokens/test/passwordless/PasswordlessConsumeCodeTest.java index b9de4ef09..884799871 100644 --- a/src/test/java/io/supertokens/test/passwordless/PasswordlessConsumeCodeTest.java +++ b/src/test/java/io/supertokens/test/passwordless/PasswordlessConsumeCodeTest.java @@ -24,12 +24,12 @@ import io.supertokens.passwordless.exceptions.IncorrectUserInputCodeException; import io.supertokens.passwordless.exceptions.RestartFlowException; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; import io.supertokens.pluginInterface.passwordless.PasswordlessStorage; -import io.supertokens.pluginInterface.passwordless.UserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -150,7 +150,7 @@ public void testConsumeUserInputCodeWithExistingUser() throws Exception { PasswordlessStorage storage = (PasswordlessStorage) StorageLayer.getStorage(process.getProcess()); - UserInfo user; + AuthRecipeUserInfo user; { Passwordless.CreateCodeResponse createCodeResponse = Passwordless.createCode(process.getProcess(), EMAIL, null, null, null); @@ -173,7 +173,7 @@ public void testConsumeUserInputCodeWithExistingUser() throws Exception { null); assertNotNull(consumeCodeResponse); assert (!consumeCodeResponse.createdNewUser); - UserInfo user2 = checkUserWithConsumeResponse(storage, consumeCodeResponse, EMAIL, null, 0); + AuthRecipeUserInfo user2 = checkUserWithConsumeResponse(storage, consumeCodeResponse, EMAIL, null, 0); assert (user.equals(user2)); } @@ -203,7 +203,7 @@ public void testConsumeLinkCodeWithExistingUser() throws Exception { PasswordlessStorage storage = (PasswordlessStorage) StorageLayer.getStorage(process.getProcess()); - UserInfo user; + AuthRecipeUserInfo user; { Passwordless.CreateCodeResponse createCodeResponse = Passwordless.createCode(process.getProcess(), EMAIL, null, null, null); @@ -224,7 +224,7 @@ public void testConsumeLinkCodeWithExistingUser() throws Exception { createCodeResponse.deviceId, createCodeResponse.deviceIdHash, null, createCodeResponse.linkCode); assertNotNull(consumeCodeResponse); assert (!consumeCodeResponse.createdNewUser); - UserInfo user2 = checkUserWithConsumeResponse(storage, consumeCodeResponse, EMAIL, null, 0); + AuthRecipeUserInfo user2 = checkUserWithConsumeResponse(storage, consumeCodeResponse, EMAIL, null, 0); assert (user.equals(user2)); } @@ -255,7 +255,7 @@ public void testConsumeCodeCleanupUserInputCodeWithEmailAndPhoneNumber() throws PasswordlessStorage storage = (PasswordlessStorage) StorageLayer.getStorage(process.getProcess()); - UserInfo user; + AuthRecipeUserInfo user; Passwordless.CreateCodeResponse createCodeResponse = Passwordless.createCode(process.getProcess(), EMAIL, null, null, null); @@ -265,10 +265,11 @@ public void testConsumeCodeCleanupUserInputCodeWithEmailAndPhoneNumber() throws createCodeResponse.deviceId, createCodeResponse.deviceIdHash, createCodeResponse.userInputCode, null); assertNotNull(consumeCodeResponse); - user = storage.getUserById(new AppIdentifier(null, null), consumeCodeResponse.user.id); - Passwordless.updateUser(process.getProcess(), user.id, null, new Passwordless.FieldUpdate(PHONE_NUMBER)); - user = storage.getUserById(new AppIdentifier(null, null), consumeCodeResponse.user.id); - assertEquals(user.phoneNumber, PHONE_NUMBER); + AuthRecipeUserInfo authUser = storage.getPrimaryUserById(new AppIdentifier(null, null), + consumeCodeResponse.user.getSupertokensUserId()); + Passwordless.updateUser(process.getProcess(), authUser.getSupertokensUserId(), null, new Passwordless.FieldUpdate(PHONE_NUMBER)); + authUser = storage.getPrimaryUserById(new AppIdentifier(null, null), consumeCodeResponse.user.getSupertokensUserId()); + assertEquals(authUser.loginMethods[0].phoneNumber, PHONE_NUMBER); // create code with email twice { @@ -329,7 +330,7 @@ public void testConsumeCodeCleanupLinkCodeWithEmailAndPhoneNumber() throws Excep PasswordlessStorage storage = (PasswordlessStorage) StorageLayer.getStorage(process.getProcess()); - UserInfo user; + AuthRecipeUserInfo user; Passwordless.CreateCodeResponse createCodeResponse = Passwordless.createCode(process.getProcess(), EMAIL, null, null, null); @@ -339,10 +340,11 @@ public void testConsumeCodeCleanupLinkCodeWithEmailAndPhoneNumber() throws Excep createCodeResponse.deviceId, createCodeResponse.deviceIdHash, createCodeResponse.userInputCode, null); assertNotNull(consumeCodeResponse); - user = storage.getUserById(new AppIdentifier(null, null), consumeCodeResponse.user.id); - Passwordless.updateUser(process.getProcess(), user.id, null, new Passwordless.FieldUpdate(PHONE_NUMBER)); - user = storage.getUserById(new AppIdentifier(null, null), consumeCodeResponse.user.id); - assertEquals(user.phoneNumber, PHONE_NUMBER); + AuthRecipeUserInfo authUser = storage.getPrimaryUserById(new AppIdentifier(null, null), + consumeCodeResponse.user.getSupertokensUserId()); + Passwordless.updateUser(process.getProcess(), authUser.getSupertokensUserId(), null, new Passwordless.FieldUpdate(PHONE_NUMBER)); + authUser = storage.getPrimaryUserById(new AppIdentifier(null, null), consumeCodeResponse.user.getSupertokensUserId()); + assertEquals(authUser.loginMethods[0].phoneNumber, PHONE_NUMBER); // create code with email twice { @@ -816,17 +818,18 @@ public void testConsumeWrongUserInputCodeExceedingMaxAttemptsWithConfigUpdate() assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } - private UserInfo checkUserWithConsumeResponse(PasswordlessStorage storage, Passwordless.ConsumeCodeResponse resp, - String email, String phoneNumber, long joinedAfter) + private AuthRecipeUserInfo checkUserWithConsumeResponse(PasswordlessStorage storage, + Passwordless.ConsumeCodeResponse resp, + String email, String phoneNumber, long joinedAfter) throws StorageQueryException { - UserInfo user = storage.getUserById(new AppIdentifier(null, null), resp.user.id); + AuthRecipeUserInfo user = storage.getPrimaryUserById(new AppIdentifier(null, null), resp.user.getSupertokensUserId()); assertNotNull(user); - assertEquals(email, resp.user.email); - assertEquals(email, user.email); + assertEquals(email, resp.user.loginMethods[0].email); + assertEquals(email, user.loginMethods[0].email); - assertEquals(phoneNumber, user.phoneNumber); - assertEquals(phoneNumber, resp.user.phoneNumber); + assertEquals(phoneNumber, user.loginMethods[0].phoneNumber); + assertEquals(phoneNumber, resp.user.loginMethods[0].phoneNumber); assert (user.timeJoined >= joinedAfter); assertEquals(user.timeJoined, resp.user.timeJoined); diff --git a/src/test/java/io/supertokens/test/passwordless/PasswordlessGetUserTest.java b/src/test/java/io/supertokens/test/passwordless/PasswordlessGetUserTest.java index 8eee9f814..59825b0d9 100644 --- a/src/test/java/io/supertokens/test/passwordless/PasswordlessGetUserTest.java +++ b/src/test/java/io/supertokens/test/passwordless/PasswordlessGetUserTest.java @@ -19,7 +19,7 @@ import io.supertokens.ProcessState; import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.passwordless.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -59,7 +59,7 @@ public void beforeEach() { */ @Test public void getUserByIdWithEmail() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -70,9 +70,9 @@ public void getUserByIdWithEmail() throws Exception { Passwordless.ConsumeCodeResponse consumeCodeResponse = createUserWith(process, EMAIL, null); - UserInfo user = Passwordless.getUserById(process.getProcess(), consumeCodeResponse.user.id); + AuthRecipeUserInfo user = Passwordless.getUserById(process.getProcess(), consumeCodeResponse.user.getSupertokensUserId()); assertNotNull(user); - assertEquals(user.email, EMAIL); + assertEquals(user.loginMethods[0].email, EMAIL); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -86,7 +86,7 @@ public void getUserByIdWithEmail() throws Exception { */ @Test public void getUserByIdWithPhoneNumber() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -97,9 +97,9 @@ public void getUserByIdWithPhoneNumber() throws Exception { Passwordless.ConsumeCodeResponse consumeCodeResponse = createUserWith(process, null, PHONE_NUMBER); - UserInfo user = Passwordless.getUserById(process.getProcess(), consumeCodeResponse.user.id); + AuthRecipeUserInfo user = Passwordless.getUserById(process.getProcess(), consumeCodeResponse.user.getSupertokensUserId()); assertNotNull(user); - assertEquals(user.phoneNumber, PHONE_NUMBER); + assertEquals(user.loginMethods[0].phoneNumber, PHONE_NUMBER); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -113,7 +113,7 @@ public void getUserByIdWithPhoneNumber() throws Exception { */ @Test public void getUserByIdWithEmailAndPhoneNumber() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -124,10 +124,10 @@ public void getUserByIdWithEmailAndPhoneNumber() throws Exception { Passwordless.ConsumeCodeResponse consumeCodeResponse = createUserWith(process, EMAIL, PHONE_NUMBER); - UserInfo user = Passwordless.getUserById(process.getProcess(), consumeCodeResponse.user.id); + AuthRecipeUserInfo user = Passwordless.getUserById(process.getProcess(), consumeCodeResponse.user.getSupertokensUserId()); assertNotNull(user); - assertEquals(user.email, EMAIL); - assertEquals(user.phoneNumber, PHONE_NUMBER); + assertEquals(user.loginMethods[0].email, EMAIL); + assertEquals(user.loginMethods[0].phoneNumber, PHONE_NUMBER); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -141,7 +141,7 @@ public void getUserByIdWithEmailAndPhoneNumber() throws Exception { */ @Test public void getUserByInvalidId() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -152,7 +152,7 @@ public void getUserByInvalidId() throws Exception { Passwordless.ConsumeCodeResponse consumeCodeResponse = createUserWith(process, EMAIL, null); - UserInfo user = Passwordless.getUserById(process.getProcess(), consumeCodeResponse.user.id + "1"); + AuthRecipeUserInfo user = Passwordless.getUserById(process.getProcess(), consumeCodeResponse.user.getSupertokensUserId() + "1"); assertNull(user); process.kill(); @@ -167,7 +167,7 @@ public void getUserByInvalidId() throws Exception { */ @Test public void getUserByEmail() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -178,9 +178,9 @@ public void getUserByEmail() throws Exception { createUserWith(process, EMAIL, null); - UserInfo user = Passwordless.getUserByEmail(process.getProcess(), EMAIL); + AuthRecipeUserInfo user = Passwordless.getUserByEmail(process.getProcess(), EMAIL); assertNotNull(user); - assertEquals(user.email, EMAIL); + assertEquals(user.loginMethods[0].email, EMAIL); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -194,7 +194,7 @@ public void getUserByEmail() throws Exception { */ @Test public void getUserByInvalidEmail() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -205,7 +205,7 @@ public void getUserByInvalidEmail() throws Exception { createUserWith(process, EMAIL, null); - UserInfo user = Passwordless.getUserByEmail(process.getProcess(), EMAIL + "a"); + AuthRecipeUserInfo user = Passwordless.getUserByEmail(process.getProcess(), EMAIL + "a"); assertNull(user); process.kill(); @@ -220,7 +220,7 @@ public void getUserByInvalidEmail() throws Exception { */ @Test public void getUserByPhoneNumber() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -231,9 +231,9 @@ public void getUserByPhoneNumber() throws Exception { createUserWith(process, null, PHONE_NUMBER); - UserInfo user = Passwordless.getUserByPhoneNumber(process.getProcess(), PHONE_NUMBER); + AuthRecipeUserInfo user = Passwordless.getUserByPhoneNumber(process.getProcess(), PHONE_NUMBER); assertNotNull(user); - assertEquals(user.phoneNumber, PHONE_NUMBER); + assertEquals(user.loginMethods[0].phoneNumber, PHONE_NUMBER); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -246,7 +246,7 @@ public void getUserByPhoneNumber() throws Exception { */ @Test public void getUserByInvalidPhoneNumber() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -257,7 +257,7 @@ public void getUserByInvalidPhoneNumber() throws Exception { createUserWith(process, null, PHONE_NUMBER); - UserInfo user = Passwordless.getUserByPhoneNumber(process.getProcess(), PHONE_NUMBER + "1"); + AuthRecipeUserInfo user = Passwordless.getUserByPhoneNumber(process.getProcess(), PHONE_NUMBER + "1"); assertNull(user); process.kill(); diff --git a/src/test/java/io/supertokens/test/passwordless/PasswordlessStorageTest.java b/src/test/java/io/supertokens/test/passwordless/PasswordlessStorageTest.java index 25b9b8e3f..9257e741f 100644 --- a/src/test/java/io/supertokens/test/passwordless/PasswordlessStorageTest.java +++ b/src/test/java/io/supertokens/test/passwordless/PasswordlessStorageTest.java @@ -18,6 +18,7 @@ import io.supertokens.ProcessState; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateUserIdException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; @@ -26,7 +27,6 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; -import io.supertokens.pluginInterface.passwordless.UserInfo; import io.supertokens.pluginInterface.passwordless.exception.*; import io.supertokens.pluginInterface.passwordless.sqlStorage.PasswordlessSQLStorage; import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; @@ -257,7 +257,7 @@ public void testCreateUserExceptions() throws Exception { storage.createUser(new TenantIdentifier(null, null, null), userId, email, null, timeJoined); storage.createUser(new TenantIdentifier(null, null, null), userId2, null, phoneNumber, timeJoined); - assertNotNull(storage.getUserById(new AppIdentifier(null, null), userId)); + assertNotNull(storage.getPrimaryUserById(new AppIdentifier(null, null), userId)); { Exception error = null; @@ -270,7 +270,7 @@ public void testCreateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof DuplicateUserIdException); - assertNull(storage.getUserByEmail(new TenantIdentifier(null, null, null), email2)); + assertEquals(0, storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), email2).length); } { @@ -284,7 +284,8 @@ public void testCreateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof DuplicateUserIdException); - assertNull(storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber2)); + assert (storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + phoneNumber2).length == 0); } { @@ -298,7 +299,7 @@ public void testCreateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof DuplicateEmailException); - assertNull(storage.getUserById(new AppIdentifier(null, null), userId3)); + assertNull(storage.getPrimaryUserById(new AppIdentifier(null, null), userId3)); } { @@ -312,7 +313,7 @@ public void testCreateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof DuplicatePhoneNumberException); - assertNull(storage.getUserById(new AppIdentifier(null, null), userId3)); + assertNull(storage.getPrimaryUserById(new AppIdentifier(null, null), userId3)); } { @@ -326,7 +327,7 @@ public void testCreateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof IllegalArgumentException); - assertNull(storage.getUserById(new AppIdentifier(null, null), userId3)); + assertNull(storage.getPrimaryUserById(new AppIdentifier(null, null), userId3)); } process.kill(); @@ -370,7 +371,7 @@ public void testUpdateUserExceptions() throws Exception { storage.createUser(new TenantIdentifier(null, null, null), userIdPhone2, null, phoneNumber2, timeJoined); - assertNotNull(storage.getUserById(new AppIdentifier(null, null), userIdEmail1)); + assertNotNull(storage.getPrimaryUserById(new AppIdentifier(null, null), userIdEmail1)); { Exception error = null; @@ -391,7 +392,7 @@ public void testUpdateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof UnknownUserIdException); - assertNull(storage.getUserById(new AppIdentifier(null, null), userIdNotExists)); + assertNull(storage.getPrimaryUserById(new AppIdentifier(null, null), userIdNotExists)); } { @@ -413,7 +414,7 @@ public void testUpdateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof UnknownUserIdException); - assertNull(storage.getUserById(new AppIdentifier(null, null), userIdNotExists)); + assertNull(storage.getPrimaryUserById(new AppIdentifier(null, null), userIdNotExists)); } { @@ -435,7 +436,8 @@ public void testUpdateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof DuplicateEmailException); - assertEquals(email, storage.getUserById(new AppIdentifier(null, null), userIdEmail1).email); + assertEquals(email, + storage.getPrimaryUserById(new AppIdentifier(null, null), userIdEmail1).loginMethods[0].email); } { @@ -456,7 +458,8 @@ public void testUpdateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof DuplicateEmailException); - assertEquals(email, storage.getUserById(new AppIdentifier(null, null), userIdEmail1).email); + assertEquals(email, + storage.getPrimaryUserById(new AppIdentifier(null, null), userIdEmail1).loginMethods[0].email); } { @@ -478,7 +481,9 @@ public void testUpdateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof DuplicatePhoneNumberException); - assertEquals(phoneNumber, storage.getUserById(new AppIdentifier(null, null), userIdPhone1).phoneNumber); + assertEquals(phoneNumber, + storage.getPrimaryUserById(new AppIdentifier(null, null), + userIdPhone1).loginMethods[0].phoneNumber); } { @@ -500,9 +505,9 @@ public void testUpdateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof DuplicatePhoneNumberException); - UserInfo userInDb = storage.getUserById(new AppIdentifier(null, null), userIdEmail1); - assertEquals(email, userInDb.email); - assertNull(userInDb.phoneNumber); + AuthRecipeUserInfo userInDb = storage.getPrimaryUserById(new AppIdentifier(null, null), userIdEmail1); + assertEquals(email, userInDb.loginMethods[0].email); + assertNull(userInDb.loginMethods[0].phoneNumber); } { @@ -523,9 +528,9 @@ public void testUpdateUserExceptions() throws Exception { assertNotNull(error); assert (error instanceof DuplicateEmailException); - UserInfo userInDb = storage.getUserById(new AppIdentifier(null, null), userIdPhone1); - assertNull(userInDb.email); - assertEquals(phoneNumber, userInDb.phoneNumber); + AuthRecipeUserInfo userInDb = storage.getPrimaryUserById(new AppIdentifier(null, null), userIdPhone1); + assertNull(userInDb.loginMethods[0].email); + assertEquals(phoneNumber, userInDb.loginMethods[0].phoneNumber); } process.kill(); @@ -557,7 +562,7 @@ public void testUpdateUser() throws Exception { storage.createUser(new TenantIdentifier(null, null, null), userId, email, null, timeJoined); - assertNotNull(storage.getUserById(new AppIdentifier(null, null), userId)); + assertNotNull(storage.getPrimaryUserById(new AppIdentifier(null, null), userId)); storage.startTransaction(con -> { try { @@ -887,16 +892,17 @@ private void checkLockingCalls(PasswordlessSQLStorage storage, TestFunction func private void checkUser(PasswordlessSQLStorage storage, String userId, String email, String phoneNumber) throws StorageQueryException { - UserInfo userById = storage.getUserById(new AppIdentifier(null, null), userId); - assertEquals(email, userById.email); - assertEquals(phoneNumber, userById.phoneNumber); + AuthRecipeUserInfo userById = storage.getPrimaryUserById(new AppIdentifier(null, null), userId); + assertEquals(email, userById.loginMethods[0].email); + assertEquals(phoneNumber, userById.loginMethods[0].phoneNumber); if (email != null) { - UserInfo user = storage.getUserByEmail(new TenantIdentifier(null, null, null), email); - assert (user.equals(userById)); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), email); + assert (user.length == 1 && user[0].equals(userById)); } if (phoneNumber != null) { - UserInfo user = storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber); - assert (user.equals(userById)); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + phoneNumber); + assert (user[0].equals(userById)); } } diff --git a/src/test/java/io/supertokens/test/passwordless/PasswordlessUpdateUserTest.java b/src/test/java/io/supertokens/test/passwordless/PasswordlessUpdateUserTest.java index 19f9e7e2a..841b82f2e 100644 --- a/src/test/java/io/supertokens/test/passwordless/PasswordlessUpdateUserTest.java +++ b/src/test/java/io/supertokens/test/passwordless/PasswordlessUpdateUserTest.java @@ -20,11 +20,11 @@ import io.supertokens.passwordless.Passwordless; import io.supertokens.passwordless.exceptions.UserWithoutContactInfoException; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.passwordless.PasswordlessStorage; -import io.supertokens.pluginInterface.passwordless.UserInfo; import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; @@ -78,15 +78,17 @@ public void updateEmailToAnExistingOne() throws Exception { createUserWith(process, EMAIL, null); createUserWith(process, alternate_email, null); - UserInfo user = storage.getUserByEmail(new TenantIdentifier(null, null, null), EMAIL); - assertNotNull(user); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), EMAIL); + assert (user.length == 1); - UserInfo user_two = storage.getUserByEmail(new TenantIdentifier(null, null, null), alternate_email); - assertNotNull(user_two); + AuthRecipeUserInfo[] user_two = storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), + alternate_email); + assert (user_two.length == 1); Exception ex = null; try { - Passwordless.updateUser(process.getProcess(), user.id, new Passwordless.FieldUpdate(alternate_email), null); + Passwordless.updateUser(process.getProcess(), user[0].getSupertokensUserId(), new Passwordless.FieldUpdate(alternate_email), + null); } catch (Exception e) { ex = e; } @@ -94,7 +96,9 @@ public void updateEmailToAnExistingOne() throws Exception { assertNotNull(ex); assert (ex instanceof DuplicateEmailException); - assertEquals(EMAIL, storage.getUserByEmail(new TenantIdentifier(null, null, null), EMAIL).email); + assertEquals(EMAIL, + storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), + EMAIL)[0].loginMethods[0].email); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -122,14 +126,16 @@ public void updatePhoneNumberToAnExistingOne() throws Exception { createUserWith(process, null, PHONE_NUMBER); createUserWith(process, null, alternate_phoneNumber); - UserInfo user = storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), PHONE_NUMBER); - assertNotNull(user); - UserInfo user_two = storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), alternate_phoneNumber); - assertNotNull(user_two); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + PHONE_NUMBER); + assert (user.length == 1); + AuthRecipeUserInfo[] user_two = storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + alternate_phoneNumber); + assert (user_two.length == 1); Exception ex = null; try { - Passwordless.updateUser(process.getProcess(), user.id, null, + Passwordless.updateUser(process.getProcess(), user[0].getSupertokensUserId(), null, new Passwordless.FieldUpdate(alternate_phoneNumber)); } catch (Exception e) { ex = e; @@ -139,7 +145,8 @@ public void updatePhoneNumberToAnExistingOne() throws Exception { assert (ex instanceof DuplicatePhoneNumberException); assertEquals(PHONE_NUMBER, - storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), PHONE_NUMBER).phoneNumber); + storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + PHONE_NUMBER)[0].loginMethods[0].phoneNumber); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -167,12 +174,13 @@ public void updateEmail() throws Exception { createUserWith(process, EMAIL, null); - UserInfo user = storage.getUserByEmail(new TenantIdentifier(null, null, null), EMAIL); - assertNotNull(user); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), EMAIL); + assert (user.length == 1); - Passwordless.updateUser(process.getProcess(), user.id, new Passwordless.FieldUpdate(alternate_email), null); + Passwordless.updateUser(process.getProcess(), user[0].getSupertokensUserId(), new Passwordless.FieldUpdate(alternate_email), null); - assertEquals(alternate_email, storage.getUserById(new AppIdentifier(null, null), user.id).email); + assertEquals(alternate_email, + storage.getPrimaryUserById(new AppIdentifier(null, null), user[0].getSupertokensUserId()).loginMethods[0].email); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -200,13 +208,15 @@ public void updatePhoneNumber() throws Exception { createUserWith(process, null, PHONE_NUMBER); - UserInfo user = storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), PHONE_NUMBER); - assertNotNull(user); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + PHONE_NUMBER); + assert (user.length == 1); - Passwordless.updateUser(process.getProcess(), user.id, null, + Passwordless.updateUser(process.getProcess(), user[0].getSupertokensUserId(), null, new Passwordless.FieldUpdate(alternate_phoneNumber)); - assertEquals(alternate_phoneNumber, storage.getUserById(new AppIdentifier(null, null), user.id).phoneNumber); + assertEquals(alternate_phoneNumber, + storage.getPrimaryUserById(new AppIdentifier(null, null), user[0].getSupertokensUserId()).loginMethods[0].phoneNumber); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -233,14 +243,15 @@ public void clearEmailSetPhoneNumber() throws Exception { createUserWith(process, EMAIL, null); - UserInfo user = storage.getUserByEmail(new TenantIdentifier(null, null, null), EMAIL); - assertNotNull(user); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), EMAIL); + assert (user.length == 1); - Passwordless.updateUser(process.getProcess(), user.id, new Passwordless.FieldUpdate(null), + Passwordless.updateUser(process.getProcess(), user[0].getSupertokensUserId(), new Passwordless.FieldUpdate(null), new Passwordless.FieldUpdate(PHONE_NUMBER)); - assertEquals(PHONE_NUMBER, storage.getUserById(new AppIdentifier(null, null), user.id).phoneNumber); - assertNull(storage.getUserById(new AppIdentifier(null, null), user.id).email); + assertEquals(PHONE_NUMBER, + storage.getPrimaryUserById(new AppIdentifier(null, null), user[0].getSupertokensUserId()).loginMethods[0].phoneNumber); + assertNull(storage.getPrimaryUserById(new AppIdentifier(null, null), user[0].getSupertokensUserId()).loginMethods[0].email); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -267,14 +278,16 @@ public void clearPhoneNumberSetEmail() throws Exception { createUserWith(process, null, PHONE_NUMBER); - UserInfo user = storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), PHONE_NUMBER); - assertNotNull(user); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + PHONE_NUMBER); + assert (user.length == 1); - Passwordless.updateUser(process.getProcess(), user.id, new Passwordless.FieldUpdate(EMAIL), + Passwordless.updateUser(process.getProcess(), user[0].getSupertokensUserId(), new Passwordless.FieldUpdate(EMAIL), new Passwordless.FieldUpdate(null)); - assertEquals(EMAIL, storage.getUserById(new AppIdentifier(null, null), user.id).email); - assertNull(storage.getUserById(new AppIdentifier(null, null), user.id).phoneNumber); + assertEquals(EMAIL, + storage.getPrimaryUserById(new AppIdentifier(null, null), user[0].getSupertokensUserId()).loginMethods[0].email); + assertNull(storage.getPrimaryUserById(new AppIdentifier(null, null), user[0].getSupertokensUserId()).loginMethods[0].phoneNumber); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -301,12 +314,13 @@ public void clearPhoneNumberAndEmail() throws Exception { createUserWith(process, null, PHONE_NUMBER); - UserInfo user = storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), PHONE_NUMBER); - assertNotNull(user); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + PHONE_NUMBER); + assert (user.length == 1); Exception ex = null; try { - Passwordless.updateUser(process.getProcess(), user.id, new Passwordless.FieldUpdate(null), + Passwordless.updateUser(process.getProcess(), user[0].getSupertokensUserId(), new Passwordless.FieldUpdate(null), new Passwordless.FieldUpdate(null)); } catch (Exception e) { ex = e; @@ -340,13 +354,13 @@ public void clearEmailOfEmailOnlyUser() throws Exception { createUserWith(process, EMAIL, null); - UserInfo user = storage.getUserByEmail(new TenantIdentifier(null, null, null), EMAIL); - assertNotNull(user); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), EMAIL); + assert (user.length == 1); Exception ex = null; try { - Passwordless.updateUser(process.getProcess(), user.id, new Passwordless.FieldUpdate(null), null); + Passwordless.updateUser(process.getProcess(), user[0].getSupertokensUserId(), new Passwordless.FieldUpdate(null), null); } catch (Exception e) { ex = e; } @@ -379,13 +393,14 @@ public void clearPhoneOfPhoneOnlyUser() throws Exception { createUserWith(process, null, PHONE_NUMBER); - UserInfo user = storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), PHONE_NUMBER); - assertNotNull(user); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + PHONE_NUMBER); + assert (user.length == 1); Exception ex = null; try { - Passwordless.updateUser(process.getProcess(), user.id, null, new Passwordless.FieldUpdate(null)); + Passwordless.updateUser(process.getProcess(), user[0].getSupertokensUserId(), null, new Passwordless.FieldUpdate(null)); } catch (Exception e) { ex = e; } @@ -419,14 +434,17 @@ public void setPhoneNumberSetEmail() throws Exception { createUserWith(process, null, PHONE_NUMBER); - UserInfo user = storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), PHONE_NUMBER); - assertNotNull(user); + AuthRecipeUserInfo[] user = storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + PHONE_NUMBER); + assert (user.length == 1); - Passwordless.updateUser(process.getProcess(), user.id, new Passwordless.FieldUpdate(EMAIL), + Passwordless.updateUser(process.getProcess(), user[0].getSupertokensUserId(), new Passwordless.FieldUpdate(EMAIL), new Passwordless.FieldUpdate(alternate_phoneNumber)); - assertEquals(EMAIL, storage.getUserById(new AppIdentifier(null, null), user.id).email); - assertEquals(alternate_phoneNumber, storage.getUserById(new AppIdentifier(null, null), user.id).phoneNumber); + assertEquals(EMAIL, + storage.getPrimaryUserById(new AppIdentifier(null, null), user[0].getSupertokensUserId()).loginMethods[0].email); + assertEquals(alternate_phoneNumber, + storage.getPrimaryUserById(new AppIdentifier(null, null), user[0].getSupertokensUserId()).loginMethods[0].phoneNumber); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java b/src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java new file mode 100644 index 000000000..750fe9b98 --- /dev/null +++ b/src/test/java/io/supertokens/test/passwordless/api/EmailVerificationTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.passwordless.api; + +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.emailverification.EmailVerification; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.*; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.passwordless.exception.DuplicateLinkCodeHashException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.Assert.*; + +public class EmailVerificationTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password) + throws DuplicateEmailException, StorageQueryException { + return EmailPassword.signUp(main, email, password); + } + + AuthRecipeUserInfo createPasswordlessUserWithEmail(Main main, String email) + throws DuplicateLinkCodeHashException, StorageQueryException, NoSuchAlgorithmException, IOException, + RestartFlowException, InvalidKeyException, Base64EncodingException, DeviceIdHashMismatchException, + StorageTransactionLogicException, IncorrectUserInputCodeException, ExpiredUserInputCodeException { + Passwordless.CreateCodeResponse code = Passwordless.createCode(main, email, null, + null, "123456"); + return Passwordless.consumeCode(main, code.deviceId, code.deviceIdHash, + code.userInputCode, null).user; + } + + @Test + public void testPasswordlessLoginSetsEmailVerified_v3_0() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String email = "test@example.com"; + + { + // Email verification is not set for CDI < 4.0 + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v3_0.get(), "passwordless"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), userId, email)); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testPasswordlessLoginSetsEmailVerified_v4_0() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String email = "test@example.com"; + + { + // Email verification is set for CDI >= 4.0 + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), userId, email)); + + EmailVerification.unverifyEmail(process.getProcess(), userId, email); + } + + { + // Email verification is set for CDI >= 4.0, for returning user + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), userId, email)); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testWithAccountLinking() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createPasswordlessUserWithEmail(process.getProcess(), "test@example.com"); + + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + EmailVerification.unverifyEmail(process.getProcess(), user2.getSupertokensUserId(), "test@example.com"); + + { + Passwordless.CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), "test@example.com", null, null, null); + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "passwordless"); + + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user2.getSupertokensUserId(), "test@example.com")); + assertTrue(response.get("user").getAsJsonObject().get("loginMethods").getAsJsonArray().get(1).getAsJsonObject().get("verified").getAsBoolean()); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessUserGetAPITest2_11.java b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessUserGetAPITest2_11.java index 0f2b3f24e..92d1b580d 100644 --- a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessUserGetAPITest2_11.java +++ b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessUserGetAPITest2_11.java @@ -18,7 +18,10 @@ import com.google.gson.JsonObject; import io.supertokens.ProcessState; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.passwordless.PasswordlessStorage; import io.supertokens.storageLayer.StorageLayer; @@ -26,6 +29,7 @@ import io.supertokens.test.Utils; import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; import io.supertokens.utils.SemVer; import io.supertokens.test.httpRequest.HttpResponseException; import org.junit.AfterClass; @@ -233,4 +237,42 @@ private static void checkUser(JsonObject resp, String userId, String email, Stri assert (System.currentTimeMillis() - 10000 < user.get("timeJoined").getAsLong()); assertEquals(3, user.entrySet().size()); } + + @Test + public void testGetUserForUsersOfOtherRecipeIds() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + AuthRecipeUserInfo user2 = ThirdParty.signInUp(process.getProcess(), "google", "googleid", "test@example.com").user; + + { + HashMap map = new HashMap<>(); + map.put("userId", user1.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user", map, 1000, 1000, null, SemVer.v2_7.get(), + "passwordless"); + assertEquals(response.get("status").getAsString(), "UNKNOWN_USER_ID_ERROR"); + } + + { + HashMap map = new HashMap<>(); + map.put("userId", user2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user", map, 1000, 1000, null, SemVer.v2_7.get(), + "passwordless"); + assertEquals(response.get("status").getAsString(), "UNKNOWN_USER_ID_ERROR"); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessUserPutAPITest2_11.java b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessUserPutAPITest2_11.java index 764257b13..f620edc0f 100644 --- a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessUserPutAPITest2_11.java +++ b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessUserPutAPITest2_11.java @@ -34,7 +34,8 @@ import org.junit.Test; import org.junit.rules.TestRule; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; public class PasswordlessUserPutAPITest2_11 { @Rule @@ -52,7 +53,7 @@ public void beforeEach() { @Test public void testBadInput() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -61,7 +62,7 @@ public void testBadInput() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; String email = "test@example.com"; String email2 = "test2@example.com"; @@ -136,7 +137,7 @@ public void testBadInput() throws Exception { @Test public void testEmailToPhone() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -145,7 +146,7 @@ public void testEmailToPhone() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; String phoneNumber = "+442071838750"; PasswordlessStorage storage = (PasswordlessStorage) StorageLayer.getStorage(process.getProcess()); @@ -164,8 +165,8 @@ public void testEmailToPhone() throws Exception { assertEquals("OK", response.get("status").getAsString()); - assertNull(storage.getUserByEmail(new TenantIdentifier(null, null, null), email)); - assertNotNull(storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber)); + assert (storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), email).length == 0); + assert (storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber).length == 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } @@ -177,7 +178,7 @@ public void testEmailToPhone() throws Exception { */ @Test public void testPhoneToEmail() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -186,7 +187,7 @@ public void testPhoneToEmail() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; String phoneNumber = "+442071838750"; String email = "email"; @@ -205,8 +206,8 @@ public void testPhoneToEmail() throws Exception { assertEquals("OK", response.get("status").getAsString()); - assertNotNull(storage.getUserByEmail(new TenantIdentifier(null, null, null), email)); - assertNull(storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber)); + assert (storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), email).length == 1); + assert (storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber).length == 0); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } @@ -218,7 +219,7 @@ public void testPhoneToEmail() throws Exception { */ @Test public void testPhoneAndEmail() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -227,7 +228,7 @@ public void testPhoneAndEmail() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; String phoneNumber = "+442071838750"; String email = "email"; String updatedPhoneNumber = "+442071838751"; @@ -248,11 +249,13 @@ public void testPhoneAndEmail() throws Exception { assertEquals("OK", response.get("status").getAsString()); - assertNull(storage.getUserByEmail(new TenantIdentifier(null, null, null), email)); - assertNull(storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber)); + assert (storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), email).length == 0); + assert (storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber).length == 0); - assertNotNull(storage.getUserByEmail(new TenantIdentifier(null, null, null), updatedEmail)); - assertNotNull(storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), updatedPhoneNumber)); + assert (storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), updatedEmail).length == 1); + assert (storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + updatedPhoneNumber).length == + 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -265,7 +268,7 @@ public void testPhoneAndEmail() throws Exception { */ @Test public void clearEmailAndPhone() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -274,7 +277,7 @@ public void clearEmailAndPhone() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; String phoneNumber = "+442071838750"; String email = "email"; @@ -314,7 +317,7 @@ public void clearEmailAndPhone() throws Exception { */ @Test public void clearEmailOfEmailOnlyUser() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -323,7 +326,7 @@ public void clearEmailOfEmailOnlyUser() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; String email = "email"; PasswordlessStorage storage = (PasswordlessStorage) StorageLayer.getStorage(process.getProcess()); @@ -362,7 +365,7 @@ public void clearEmailOfEmailOnlyUser() throws Exception { */ @Test public void clearPhoneNUmberOfPhoneNumberOnlyUser() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -371,7 +374,7 @@ public void clearPhoneNUmberOfPhoneNumberOnlyUser() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; String phoneNumber = "+91898989898"; PasswordlessStorage storage = (PasswordlessStorage) StorageLayer.getStorage(process.getProcess()); @@ -409,7 +412,7 @@ public void clearPhoneNUmberOfPhoneNumberOnlyUser() throws Exception { */ @Test public void clearEmail() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -418,7 +421,7 @@ public void clearEmail() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; String email = "email"; String phoneNumber = "+9189898989"; @@ -436,8 +439,8 @@ public void clearEmail() throws Exception { assertEquals("OK", response.get("status").getAsString()); - assertNull(storage.getUserByEmail(new TenantIdentifier(null, null, null), email)); - assertNotNull(storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber)); + assert (storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), email).length == 0); + assert (storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber).length == 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -450,7 +453,7 @@ public void clearEmail() throws Exception { */ @Test public void clearPhone() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -459,7 +462,7 @@ public void clearPhone() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; String email = "email"; String phoneNumber = "+9189898989"; @@ -477,8 +480,8 @@ public void clearPhone() throws Exception { assertEquals("OK", response.get("status").getAsString()); - assertNotNull(storage.getUserByEmail(new TenantIdentifier(null, null, null), email)); - assertNull(storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber)); + assert (storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), email).length == 1); + assert (storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber).length == 0); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -491,7 +494,7 @@ public void clearPhone() throws Exception { */ @Test public void updateNothing() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -500,7 +503,7 @@ public void updateNothing() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; String email = "email"; String phoneNumber = "+9189898989"; @@ -517,8 +520,8 @@ public void updateNothing() throws Exception { assertEquals("OK", response.get("status").getAsString()); - assertNotNull(storage.getUserByEmail(new TenantIdentifier(null, null, null), email)); - assertNotNull(storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber)); + assert (storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), email).length == 1); + assert (storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber).length == 1); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -531,7 +534,7 @@ public void updateNothing() throws Exception { */ @Test public void testUpdateEmail() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -540,7 +543,7 @@ public void testUpdateEmail() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; PasswordlessStorage storage = (PasswordlessStorage) StorageLayer.getStorage(process.getProcess()); String email = "email"; @@ -558,8 +561,8 @@ public void testUpdateEmail() throws Exception { assertEquals("OK", response.get("status").getAsString()); - assertNotNull(storage.getUserByEmail(new TenantIdentifier(null, null, null), updated_email)); - assertNull(storage.getUserByEmail(new TenantIdentifier(null, null, null), email)); + assert (storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), updated_email).length == 1); + assert (storage.listPrimaryUsersByEmail(new TenantIdentifier(null, null, null), email).length == 0); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -572,7 +575,7 @@ public void testUpdateEmail() throws Exception { */ @Test public void testUpdatePhoneNumber() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -581,7 +584,7 @@ public void testUpdatePhoneNumber() throws Exception { return; } - String userId = "userId"; + String userId = "6347c997-4cc9-4f95-94c9-b96e2c65aefc"; String phoneNumber = "+442071838750"; String updatedPhoneNumber = "+442071838751"; @@ -599,8 +602,9 @@ public void testUpdatePhoneNumber() throws Exception { assertEquals("OK", response.get("status").getAsString()); - assertNotNull(storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), updatedPhoneNumber)); - assertNull(storage.getUserByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber)); + assert (storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), + updatedPhoneNumber).length == 1); + assert (storage.listPrimaryUsersByPhoneNumber(new TenantIdentifier(null, null, null), phoneNumber).length == 0); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); diff --git a/src/test/java/io/supertokens/test/session/AccessTokenTest.java b/src/test/java/io/supertokens/test/session/AccessTokenTest.java index dc2c3ce34..30247227a 100644 --- a/src/test/java/io/supertokens/test/session/AccessTokenTest.java +++ b/src/test/java/io/supertokens/test/session/AccessTokenTest.java @@ -106,7 +106,7 @@ public void testCreateSessionWithDataExpireGetAccessTokenAndCheckPayload() throw // check payload is fine assertEquals(accessTokenInfo.userData, userDataInJWT); - assertEquals(accessTokenInfo.userId, userId); + assertEquals(accessTokenInfo.recipeUserId, userId); process.kill(); assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STOPPED)); @@ -143,7 +143,7 @@ public void testCreateSessionV2WithDataExpireGetAccessTokenAndCheckPayload() thr // check payload is fine assertEquals(accessTokenInfo.userData, userDataInJWT); - assertEquals(accessTokenInfo.userId, userId); + assertEquals(accessTokenInfo.recipeUserId, userId); process.kill(); assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STOPPED)); @@ -267,7 +267,7 @@ public void inputOutputTest() throws Exception { AccessToken.getLatestVersion(), false); AccessTokenInfo info = AccessToken.getInfoFromAccessToken(process.getProcess(), newToken.token, true); assertEquals("sessionHandle", info.sessionHandle); - assertEquals("userId", info.userId); + assertEquals("userId", info.recipeUserId); assertEquals("refreshTokenHash1", info.refreshTokenHash1); assertEquals("parentRefreshTokenHash1", info.parentRefreshTokenHash1); assertEquals(testValue, info.userData.get("key").getAsString()); @@ -304,7 +304,7 @@ public void inputOutputTestStatic() throws Exception { System.out.println(newToken.token); AccessTokenInfo info = AccessToken.getInfoFromAccessToken(process.getProcess(), newToken.token, true); assertEquals("sessionHandle", info.sessionHandle); - assertEquals("userId", info.userId); + assertEquals("userId", info.recipeUserId); assertEquals("refreshTokenHash1", info.refreshTokenHash1); assertEquals("parentRefreshTokenHash1", info.parentRefreshTokenHash1); assertEquals(testValue, info.userData.get("key").getAsString()); @@ -339,7 +339,7 @@ public void inputOutputTestV2() throws Exception { AccessToken.VERSION.V2, false); AccessTokenInfo info = AccessToken.getInfoFromAccessToken(process.getProcess(), newToken.token, true); assertEquals("sessionHandle", info.sessionHandle); - assertEquals("userId", info.userId); + assertEquals("userId", info.recipeUserId); assertEquals("refreshTokenHash1", info.refreshTokenHash1); assertEquals("parentRefreshTokenHash1", info.parentRefreshTokenHash1); assertEquals(testValue, info.userData.get("key").getAsString()); @@ -372,7 +372,7 @@ public void inputOutputTestv1() throws InterruptedException, InvalidKeyException "refreshTokenHash1", "parentRefreshTokenHash1", jsonObj, "antiCsrfToken"); AccessTokenInfo info = AccessToken.getInfoFromAccessToken(process.getProcess(), newToken.token, true); assertEquals("sessionHandle", info.sessionHandle); - assertEquals("userId", info.userId); + assertEquals("userId", info.recipeUserId); assertEquals("refreshTokenHash1", info.refreshTokenHash1); assertEquals("parentRefreshTokenHash1", info.parentRefreshTokenHash1); assertEquals(testValue, info.userData.get("key").getAsString()); diff --git a/src/test/java/io/supertokens/test/session/SessionTest2.java b/src/test/java/io/supertokens/test/session/SessionTest2.java index 43717bdf3..5643cfc16 100644 --- a/src/test/java/io/supertokens/test/session/SessionTest2.java +++ b/src/test/java/io/supertokens/test/session/SessionTest2.java @@ -21,10 +21,7 @@ import io.supertokens.Main; import io.supertokens.ProcessState; import io.supertokens.exceptions.TokenTheftDetectedException; -import io.supertokens.exceptions.TryRefreshTokenException; import io.supertokens.exceptions.UnauthorisedException; -import io.supertokens.pluginInterface.exceptions.StorageQueryException; -import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.session.SessionStorage; import io.supertokens.session.Session; @@ -39,16 +36,6 @@ import org.junit.Test; import org.junit.rules.TestRule; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; - import static junit.framework.TestCase.assertEquals; import static org.junit.Assert.*; @@ -82,7 +69,8 @@ public void tokenTheft_S1_R1_S2_R1() throws Exception { JsonObject userDataInDatabase = new JsonObject(); userDataInDatabase.addProperty("key", "value"); - SessionInformationHolder sessionInfo = Session.createNewSession(main, userId, userDataInJWT, userDataInDatabase); + SessionInformationHolder sessionInfo = Session.createNewSession(main, userId, userDataInJWT, + userDataInDatabase); assert sessionInfo.refreshToken != null; assert sessionInfo.accessToken != null; @@ -97,10 +85,11 @@ public void tokenTheft_S1_R1_S2_R1() throws Exception { assertNotEquals(sessionObj.accessToken.token, newRefreshedSession.accessToken.token); try { - Session.refreshSession(main, sessionInfo.refreshToken.token, sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion()); + Session.refreshSession(main, sessionInfo.refreshToken.token, sessionInfo.antiCsrfToken, false, + AccessToken.getLatestVersion()); } catch (TokenTheftDetectedException e) { assertEquals(e.sessionHandle, sessionInfo.session.handle); - assertEquals(e.userId, sessionInfo.session.userId); + assertEquals(e.recipeUserId, sessionInfo.session.userId); } process.kill(); @@ -123,7 +112,8 @@ public void tokenTheft_S1_R1_R2_R1() throws Exception { JsonObject userDataInDatabase = new JsonObject(); userDataInDatabase.addProperty("key", "value"); - SessionInformationHolder sessionInfo = Session.createNewSession(main, userId, userDataInJWT, userDataInDatabase); + SessionInformationHolder sessionInfo = Session.createNewSession(main, userId, userDataInJWT, + userDataInDatabase); assert sessionInfo.refreshToken != null; assert sessionInfo.accessToken != null; @@ -133,15 +123,17 @@ public void tokenTheft_S1_R1_R2_R1() throws Exception { assert newRefreshedSession1.accessToken != null; SessionInformationHolder newRefreshedSession2 = Session.refreshSession(main, - newRefreshedSession1.refreshToken.token, newRefreshedSession1.antiCsrfToken, false, AccessToken.getLatestVersion()); + newRefreshedSession1.refreshToken.token, newRefreshedSession1.antiCsrfToken, false, + AccessToken.getLatestVersion()); assert newRefreshedSession2.refreshToken != null; assert newRefreshedSession2.accessToken != null; try { - Session.refreshSession(main, sessionInfo.refreshToken.token, sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion()); + Session.refreshSession(main, sessionInfo.refreshToken.token, sessionInfo.antiCsrfToken, false, + AccessToken.getLatestVersion()); } catch (TokenTheftDetectedException e) { assertEquals(e.sessionHandle, sessionInfo.session.handle); - assertEquals(e.userId, sessionInfo.session.userId); + assertEquals(e.recipeUserId, sessionInfo.session.userId); } process.kill(); @@ -174,7 +166,8 @@ public void updateSessionInfo() throws Exception { JsonArray arr = new JsonArray(); userDataInDatabase2.add("key3", arr); - Session.updateSession(process.getProcess(), sessionInfo.session.handle, userDataInDatabase2, null, AccessToken.getLatestVersion()); + Session.updateSession(process.getProcess(), sessionInfo.session.handle, userDataInDatabase2, null, + AccessToken.getLatestVersion()); JsonObject sessionDataAfterUpdate = Session.getSessionData(process.getProcess(), sessionInfo.session.handle); assertEquals(userDataInDatabase2.toString(), sessionDataAfterUpdate.toString()); diff --git a/src/test/java/io/supertokens/test/thirdparty/ThirdPartyTest.java b/src/test/java/io/supertokens/test/thirdparty/ThirdPartyTest.java index 7d7ceaf76..bf1da6392 100644 --- a/src/test/java/io/supertokens/test/thirdparty/ThirdPartyTest.java +++ b/src/test/java/io/supertokens/test/thirdparty/ThirdPartyTest.java @@ -19,8 +19,9 @@ import io.supertokens.ProcessState; import io.supertokens.emailverification.EmailVerification; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.pluginInterface.thirdparty.UserInfo; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException; import io.supertokens.pluginInterface.thirdparty.sqlStorage.ThirdPartySQLStorage; @@ -102,8 +103,8 @@ public void testSignUpWithEmailNotVerifiedAndCheckEmailIsNotVerified() throws Ex thirdPartyUserId, email); checkSignInUpResponse(signUpResponse, thirdPartyUserId, thirdPartyId, email, true); - assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.id, - signUpResponse.user.email)); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.getSupertokensUserId(), + signUpResponse.user.loginMethods[0].email)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -154,8 +155,8 @@ public void testSignUpWithFalseVerifiedEmailAndSignInWithVerifiedEmail() throws thirdPartyUserId, email); checkSignInUpResponse(signUpResponse, thirdPartyUserId, thirdPartyId, email, true); - assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.id, - signUpResponse.user.email)); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.getSupertokensUserId(), + signUpResponse.user.loginMethods[0].email)); ThirdParty.SignInUpResponse signInResponse = ThirdParty.signInUp(process.getProcess(), thirdPartyId, thirdPartyUserId, email); @@ -192,11 +193,11 @@ public void testUpdatingEmailAndCheckVerification() throws Exception { thirdPartyUserId, email_2); checkSignInUpResponse(signInResponse, thirdPartyUserId, thirdPartyId, email_2, false); - assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signInResponse.user.id, - signInResponse.user.email)); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signInResponse.user.getSupertokensUserId(), + signInResponse.user.loginMethods[0].email)); - UserInfo updatedUserInfo = ThirdParty.getUser(process.getProcess(), thirdPartyId, thirdPartyUserId); - assertEquals(updatedUserInfo.email, email_2); + AuthRecipeUserInfo updatedUserInfo = ThirdParty.getUser(process.getProcess(), thirdPartyId, thirdPartyUserId); + assertEquals(updatedUserInfo.loginMethods[0].email, email_2); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -291,7 +292,7 @@ public void testSignUpWithSameThirdPartyThirdPartyUserIdException() throws Excep try { ((ThirdPartySQLStorage) StorageLayer.getStorage(process.getProcess())) .signUp(new TenantIdentifier(null, null, null), io.supertokens.utils.Utils.getUUID(), email, - new UserInfo.ThirdParty(thirdPartyId, thirdPartyUserId), System.currentTimeMillis()); + new LoginMethod.ThirdParty(thirdPartyId, thirdPartyUserId), System.currentTimeMillis()); throw new Exception("Should not come here"); } catch (DuplicateThirdPartyUserException ignored) { } @@ -323,8 +324,9 @@ public void testSignUpWithSameUserIdAndCheckDuplicateUserIdException() throws Ex checkSignInUpResponse(signUpResponse, thirdPartyUserId, thirdPartyId, email, true); try { ((ThirdPartySQLStorage) StorageLayer.getStorage(process.getProcess())) - .signUp(new TenantIdentifier(null, null, null), signUpResponse.user.id, email, - new UserInfo.ThirdParty("newThirdParty", "newThirdPartyUserId"), System.currentTimeMillis()); + .signUp(new TenantIdentifier(null, null, null), signUpResponse.user.getSupertokensUserId(), email, + new LoginMethod.ThirdParty("newThirdParty", "newThirdPartyUserId"), + System.currentTimeMillis()); throw new Exception("Should not come here"); } catch (DuplicateUserIdException ignored) { } @@ -386,20 +388,23 @@ public void testGetUserFunctions() throws Exception { checkSignInUpResponse(signUpResponse, thirdPartyUserId, thirdPartyId, email, true); - UserInfo getUserInfoFromId = ThirdParty.getUser(process.getProcess(), signUpResponse.user.id); - assertEquals(getUserInfoFromId.id, signUpResponse.user.id); + AuthRecipeUserInfo getUserInfoFromId = ThirdParty.getUser(process.getProcess(), signUpResponse.user.getSupertokensUserId()); + assertEquals(getUserInfoFromId.getSupertokensUserId(), signUpResponse.user.getSupertokensUserId()); assertEquals(getUserInfoFromId.timeJoined, signUpResponse.user.timeJoined); - assertEquals(getUserInfoFromId.email, signUpResponse.user.email); - assertEquals(getUserInfoFromId.thirdParty.userId, signUpResponse.user.thirdParty.userId); - assertEquals(getUserInfoFromId.thirdParty.id, signUpResponse.user.thirdParty.id); - - UserInfo getUserInfoFromThirdParty = ThirdParty.getUser(process.getProcess(), signUpResponse.user.thirdParty.id, - signUpResponse.user.thirdParty.userId); - assertEquals(getUserInfoFromThirdParty.id, signUpResponse.user.id); + assertEquals(getUserInfoFromId.loginMethods[0].email, signUpResponse.user.loginMethods[0].email); + assertEquals(getUserInfoFromId.loginMethods[0].thirdParty.userId, signUpResponse.user.loginMethods[0].thirdParty.userId); + assertEquals(getUserInfoFromId.loginMethods[0].thirdParty.id, signUpResponse.user.loginMethods[0].thirdParty.id); + + AuthRecipeUserInfo getUserInfoFromThirdParty = ThirdParty.getUser(process.getProcess(), + signUpResponse.user.loginMethods[0].thirdParty.id, + signUpResponse.user.loginMethods[0].thirdParty.userId); + assertEquals(getUserInfoFromThirdParty.getSupertokensUserId(), signUpResponse.user.getSupertokensUserId()); assertEquals(getUserInfoFromThirdParty.timeJoined, signUpResponse.user.timeJoined); - assertEquals(getUserInfoFromThirdParty.email, signUpResponse.user.email); - assertEquals(getUserInfoFromThirdParty.thirdParty.userId, signUpResponse.user.thirdParty.userId); - assertEquals(getUserInfoFromThirdParty.thirdParty.id, signUpResponse.user.thirdParty.id); + assertEquals(getUserInfoFromThirdParty.loginMethods[0].email, signUpResponse.user.loginMethods[0].email); + assertEquals(getUserInfoFromThirdParty.loginMethods[0].thirdParty.userId, + signUpResponse.user.loginMethods[0].thirdParty.userId); + assertEquals(getUserInfoFromThirdParty.loginMethods[0].thirdParty.id, + signUpResponse.user.loginMethods[0].thirdParty.id); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -408,10 +413,10 @@ public void testGetUserFunctions() throws Exception { public static void checkSignInUpResponse(ThirdParty.SignInUpResponse response, String thirdPartyUserId, String thirdPartyId, String email, boolean createNewUser) { assertEquals(response.createdNewUser, createNewUser); - assertNotNull(response.user.id); - assertEquals(response.user.thirdParty.userId, thirdPartyUserId); - assertEquals(response.user.thirdParty.id, thirdPartyId); - assertEquals(response.user.email, email); + assertNotNull(response.user.getSupertokensUserId()); + assertEquals(response.user.loginMethods[0].thirdParty.userId, thirdPartyUserId); + assertEquals(response.user.loginMethods[0].thirdParty.id, thirdPartyId); + assertEquals(response.user.loginMethods[0].email, email); } } diff --git a/src/test/java/io/supertokens/test/thirdparty/ThirdPartyTest2_7.java b/src/test/java/io/supertokens/test/thirdparty/ThirdPartyTest2_7.java index cecd0f6c2..d7b7818cc 100644 --- a/src/test/java/io/supertokens/test/thirdparty/ThirdPartyTest2_7.java +++ b/src/test/java/io/supertokens/test/thirdparty/ThirdPartyTest2_7.java @@ -19,8 +19,9 @@ import io.supertokens.ProcessState; import io.supertokens.emailverification.EmailVerification; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.pluginInterface.thirdparty.UserInfo; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.pluginInterface.thirdparty.exception.DuplicateUserIdException; import io.supertokens.pluginInterface.thirdparty.sqlStorage.ThirdPartySQLStorage; @@ -102,8 +103,8 @@ public void testSignUpWithEmailNotVerifiedAndCheckEmailIsNotVerified() throws Ex thirdPartyUserId, email, false); checkSignInUpResponse(signUpResponse, thirdPartyUserId, thirdPartyId, email, true); - assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.id, - signUpResponse.user.email)); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.getSupertokensUserId(), + signUpResponse.user.loginMethods[0].email)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -128,8 +129,8 @@ public void testSignUpWithVerifiedEmailTrueAndCheckSignUp() throws Exception { ThirdParty.SignInUpResponse signUpResponse = ThirdParty.signInUp2_7(process.getProcess(), thirdPartyId, thirdPartyUserId, email, true); checkSignInUpResponse(signUpResponse, thirdPartyUserId, thirdPartyId, email, true); - assertTrue(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.id, - signUpResponse.user.email)); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.getSupertokensUserId(), + signUpResponse.user.loginMethods[0].email)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -156,15 +157,15 @@ public void testSignUpWithFalseVerifiedEmailAndSignInWithVerifiedEmail() throws thirdPartyUserId, email, false); checkSignInUpResponse(signUpResponse, thirdPartyUserId, thirdPartyId, email, true); - assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.id, - signUpResponse.user.email)); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.getSupertokensUserId(), + signUpResponse.user.loginMethods[0].email)); ThirdParty.SignInUpResponse signInResponse = ThirdParty.signInUp2_7(process.getProcess(), thirdPartyId, thirdPartyUserId, email, true); checkSignInUpResponse(signInResponse, thirdPartyUserId, thirdPartyId, email, false); - assertTrue(EmailVerification.isEmailVerified(process.getProcess(), signInResponse.user.id, - signInResponse.user.email)); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), signInResponse.user.getSupertokensUserId(), + signInResponse.user.loginMethods[0].email)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -193,18 +194,18 @@ public void testUpdatingEmailAndCheckVerification() throws Exception { thirdPartyUserId, email_1, true); checkSignInUpResponse(signUpResponse, thirdPartyUserId, thirdPartyId, email_1, true); - assertTrue(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.id, - signUpResponse.user.email)); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.getSupertokensUserId(), + signUpResponse.user.loginMethods[0].email)); ThirdParty.SignInUpResponse signInResponse = ThirdParty.signInUp2_7(process.getProcess(), thirdPartyId, thirdPartyUserId, email_2, false); checkSignInUpResponse(signInResponse, thirdPartyUserId, thirdPartyId, email_2, false); - assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signInResponse.user.id, - signInResponse.user.email)); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), signInResponse.user.getSupertokensUserId(), + signInResponse.user.loginMethods[0].email)); - UserInfo updatedUserInfo = ThirdParty.getUser(process.getProcess(), thirdPartyId, thirdPartyUserId); - assertEquals(updatedUserInfo.email, email_2); + AuthRecipeUserInfo updatedUserInfo = ThirdParty.getUser(process.getProcess(), thirdPartyId, thirdPartyUserId); + assertEquals(updatedUserInfo.loginMethods[0].email, email_2); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -299,7 +300,7 @@ public void testSignUpWithSameThirdPartyThirdPartyUserIdException() throws Excep try { ((ThirdPartySQLStorage) StorageLayer.getStorage(process.getProcess())) .signUp(new TenantIdentifier(null, null, null), io.supertokens.utils.Utils.getUUID(), email, - new UserInfo.ThirdParty(thirdPartyId, thirdPartyUserId), System.currentTimeMillis()); + new LoginMethod.ThirdParty(thirdPartyId, thirdPartyUserId), System.currentTimeMillis()); throw new Exception("Should not come here"); } catch (DuplicateThirdPartyUserException ignored) { } @@ -331,8 +332,9 @@ public void testSignUpWithSameUserIdAndCheckDuplicateUserIdException() throws Ex checkSignInUpResponse(signUpResponse, thirdPartyUserId, thirdPartyId, email, true); try { ((ThirdPartySQLStorage) StorageLayer.getStorage(process.getProcess())) - .signUp(new TenantIdentifier(null, null, null), signUpResponse.user.id, email, - new UserInfo.ThirdParty("newThirdParty", "newThirdPartyUserId"), System.currentTimeMillis()); + .signUp(new TenantIdentifier(null, null, null), signUpResponse.user.getSupertokensUserId(), email, + new LoginMethod.ThirdParty("newThirdParty", "newThirdPartyUserId"), + System.currentTimeMillis()); throw new Exception("Should not come here"); } catch (DuplicateUserIdException ignored) { } @@ -362,15 +364,15 @@ public void testSignUpWithVerifiedEmailSignInWithUnVerifiedEmail() throws Except thirdPartyUserId, email, true); checkSignInUpResponse(signUpResponse, thirdPartyUserId, thirdPartyId, email, true); - assertTrue(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.id, - signUpResponse.user.email)); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), signUpResponse.user.getSupertokensUserId(), + signUpResponse.user.loginMethods[0].email)); ThirdParty.SignInUpResponse signInResponse = ThirdParty.signInUp2_7(process.getProcess(), thirdPartyId, thirdPartyUserId, email, false); checkSignInUpResponse(signInResponse, thirdPartyUserId, thirdPartyId, email, false); - assertTrue(EmailVerification.isEmailVerified(process.getProcess(), signInResponse.user.id, - signInResponse.user.email)); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), signInResponse.user.getSupertokensUserId(), + signInResponse.user.loginMethods[0].email)); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -398,20 +400,23 @@ public void testGetUserFunctions() throws Exception { checkSignInUpResponse(signUpResponse, thirdPartyUserId, thirdPartyId, email, true); - UserInfo getUserInfoFromId = ThirdParty.getUser(process.getProcess(), signUpResponse.user.id); - assertEquals(getUserInfoFromId.id, signUpResponse.user.id); + AuthRecipeUserInfo getUserInfoFromId = ThirdParty.getUser(process.getProcess(), signUpResponse.user.getSupertokensUserId()); + assertEquals(getUserInfoFromId.getSupertokensUserId(), signUpResponse.user.getSupertokensUserId()); assertEquals(getUserInfoFromId.timeJoined, signUpResponse.user.timeJoined); - assertEquals(getUserInfoFromId.email, signUpResponse.user.email); - assertEquals(getUserInfoFromId.thirdParty.userId, signUpResponse.user.thirdParty.userId); - assertEquals(getUserInfoFromId.thirdParty.id, signUpResponse.user.thirdParty.id); - - UserInfo getUserInfoFromThirdParty = ThirdParty.getUser(process.getProcess(), signUpResponse.user.thirdParty.id, - signUpResponse.user.thirdParty.userId); - assertEquals(getUserInfoFromThirdParty.id, signUpResponse.user.id); + assertEquals(getUserInfoFromId.loginMethods[0].email, signUpResponse.user.loginMethods[0].email); + assertEquals(getUserInfoFromId.loginMethods[0].thirdParty.userId, signUpResponse.user.loginMethods[0].thirdParty.userId); + assertEquals(getUserInfoFromId.loginMethods[0].thirdParty.id, signUpResponse.user.loginMethods[0].thirdParty.id); + + AuthRecipeUserInfo getUserInfoFromThirdParty = ThirdParty.getUser(process.getProcess(), + signUpResponse.user.loginMethods[0].thirdParty.id, + signUpResponse.user.loginMethods[0].thirdParty.userId); + assertEquals(getUserInfoFromThirdParty.getSupertokensUserId(), signUpResponse.user.getSupertokensUserId()); assertEquals(getUserInfoFromThirdParty.timeJoined, signUpResponse.user.timeJoined); - assertEquals(getUserInfoFromThirdParty.email, signUpResponse.user.email); - assertEquals(getUserInfoFromThirdParty.thirdParty.userId, signUpResponse.user.thirdParty.userId); - assertEquals(getUserInfoFromThirdParty.thirdParty.id, signUpResponse.user.thirdParty.id); + assertEquals(getUserInfoFromThirdParty.loginMethods[0].email, signUpResponse.user.loginMethods[0].email); + assertEquals(getUserInfoFromThirdParty.loginMethods[0].thirdParty.userId, + signUpResponse.user.loginMethods[0].thirdParty.userId); + assertEquals(getUserInfoFromThirdParty.loginMethods[0].thirdParty.id, + signUpResponse.user.loginMethods[0].thirdParty.id); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); @@ -420,10 +425,10 @@ public void testGetUserFunctions() throws Exception { public static void checkSignInUpResponse(ThirdParty.SignInUpResponse response, String thirdPartyUserId, String thirdPartyId, String email, boolean createNewUser) { assertEquals(response.createdNewUser, createNewUser); - assertNotNull(response.user.id); - assertEquals(response.user.thirdParty.userId, thirdPartyUserId); - assertEquals(response.user.thirdParty.id, thirdPartyId); - assertEquals(response.user.email, email); + assertNotNull(response.user.getSupertokensUserId()); + assertEquals(response.user.loginMethods[0].thirdParty.userId, thirdPartyUserId); + assertEquals(response.user.loginMethods[0].thirdParty.id, thirdPartyId); + assertEquals(response.user.loginMethods[0].email, email); } } diff --git a/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java b/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java new file mode 100644 index 000000000..b270670bf --- /dev/null +++ b/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.thirdparty.api; + +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.emailverification.EmailVerification; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.test.httpRequest.HttpResponseException; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + +public class EmailVerificationTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + AuthRecipeUserInfo createEmailPasswordUser(Main main, String email, String password) + throws DuplicateEmailException, StorageQueryException { + return EmailPassword.signUp(main, email, password); + } + + AuthRecipeUserInfo createThirdPartyUser(Main main, String thirdPartyId, String thirdPartyUserId, String email) + throws EmailChangeNotAllowedException, StorageQueryException { + return ThirdParty.signInUp(main, thirdPartyId, thirdPartyUserId, email).user; + } + + @Test + public void testEmailVerificationOnSignInUp_v3_0() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v3_0.get(), "thirdparty"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), userId, "test@example.com")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testEmailVerificationOnSignInUp_v4_0() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { // Expects emailVerified in emailObject + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + try { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertTrue(e.getMessage().contains("Http error. Status Code: 400. Message: Field name 'isVerified' is invalid in JSON input")); + } + } + + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", true); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), userId, "test@example.com")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testEmailVerificationStateDoesNotChangeWhenFalseIsPassed() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), userId, "test@example.com")); + } + + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", true); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), userId, "test@example.com")); + } + + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), userId, "test@example.com")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testWithAccountLinking() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = createEmailPasswordUser(process.getProcess(), "test@example.com", "password"); + Thread.sleep(50); + AuthRecipeUserInfo user2 = createThirdPartyUser(process.getProcess(), "google", "google-user", "test@example.com"); + + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + EmailVerification.unverifyEmail(process.getProcess(), user2.getSupertokensUserId(), "test@example.com"); + + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", true); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000000, 1000000, null, + SemVer.v4_0.get(), "thirdparty"); + + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), user2.getSupertokensUserId(), "test@example.com")); + } + } +} diff --git a/src/test/java/io/supertokens/test/thirdparty/api/ThirdPartyGetUserAPITest2_7.java b/src/test/java/io/supertokens/test/thirdparty/api/ThirdPartyGetUserAPITest2_7.java index 241bb326c..9aab47c5a 100644 --- a/src/test/java/io/supertokens/test/thirdparty/api/ThirdPartyGetUserAPITest2_7.java +++ b/src/test/java/io/supertokens/test/thirdparty/api/ThirdPartyGetUserAPITest2_7.java @@ -18,7 +18,10 @@ import com.google.gson.JsonObject; import io.supertokens.ProcessState; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -139,7 +142,7 @@ public void testGoodInput() throws Exception { // query with userId { HashMap QueryParams = new HashMap<>(); - QueryParams.put("userId", signUpResponse.user.id); + QueryParams.put("userId", signUpResponse.user.getSupertokensUserId()); JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", "http://localhost:3567/recipe/user", QueryParams, 1000, 1000, null, @@ -205,6 +208,46 @@ public void testAllTypesOfOutput() throws Exception { } } + @Test + public void testGetUserForUsersOfOtherRecipeIds() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user1 = EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + Passwordless.CreateCodeResponse user2code = Passwordless.createCode(process.getProcess(), "test@example.com", + null, null, null); + AuthRecipeUserInfo user2 = Passwordless.consumeCode(process.getProcess(), user2code.deviceId, user2code.deviceIdHash, user2code.userInputCode, null).user; + + { + HashMap map = new HashMap<>(); + map.put("userId", user1.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user", map, 1000, 1000, null, SemVer.v2_7.get(), + "thirdparty"); + assertEquals(response.get("status").getAsString(), "UNKNOWN_USER_ID_ERROR"); + } + + { + HashMap map = new HashMap<>(); + map.put("userId", user2.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user", map, 1000, 1000, null, SemVer.v2_7.get(), + "thirdparty"); + assertEquals(response.get("status").getAsString(), "UNKNOWN_USER_ID_ERROR"); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + public static void checkUser(JsonObject user, String thirdPartyId, String thirdPartyUserId, String email) { assertNotNull(user.get("id")); assertNotNull(user.get("timeJoined")); diff --git a/src/test/java/io/supertokens/test/thirdparty/api/ThirdPartySignInUpAPITest4_0.java b/src/test/java/io/supertokens/test/thirdparty/api/ThirdPartySignInUpAPITest4_0.java new file mode 100644 index 000000000..290224130 --- /dev/null +++ b/src/test/java/io/supertokens/test/thirdparty/api/ThirdPartySignInUpAPITest4_0.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test.thirdparty.api; + +import com.google.gson.JsonObject; +import io.supertokens.ActiveUsers; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.test.httpRequest.HttpRequestForTesting; +import io.supertokens.thirdparty.ThirdParty; +import io.supertokens.utils.SemVer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.*; + + +public class ThirdPartySignInUpAPITest4_0 { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + // good input + // failure condition: test fails if signinup response does not match api spec + @Test + public void testGoodInput() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + long startTs = System.currentTimeMillis(); + + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + { + assert (response.get("status").getAsString().equals("OK")); + assert (response.get("createdNewUser").getAsBoolean()); + JsonObject jsonUser = response.get("user").getAsJsonObject(); + assertNotNull(jsonUser.get("id")); + assertNotNull(jsonUser.get("timeJoined")); + assert (!jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 1); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("test@example.com")); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 1); + assert (jsonUser.get("thirdParty").getAsJsonArray().get(0).getAsJsonObject().get("id").getAsString() + .equals("google")); + assert (jsonUser.get("thirdParty").getAsJsonArray().get(0).getAsJsonObject().get("userId").getAsString() + .equals("google-user")); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 1); + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assertNotNull(lM.get("timeJoined")); + assertNotNull(lM.get("recipeUserId")); + assertEquals(lM.get("recipeId").getAsString(), "thirdparty"); + assertEquals(lM.get("email").getAsString(), "test@example.com"); + assert (lM.get("thirdParty").getAsJsonObject().get("id").getAsString() + .equals("google")); + assert (lM.get("thirdParty").getAsJsonObject().get("userId").getAsString() + .equals("google-user")); + assert (lM.entrySet().size() == 7); + } + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testNotAllowedUpdateOfEmail() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + AuthRecipeUserInfo user0 = EmailPassword.signUp(process.getProcess(), "someemail1@gmail.com", "somePass"); + AuthRecipe.createPrimaryUser(process.main, user0.getSupertokensUserId()); + + ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.getProcess(), "google", "user", + "someemail@gmail.com"); + AuthRecipe.createPrimaryUser(process.main, signInUpResponse.user.getSupertokensUserId()); + + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "someemail1@gmail.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + assert (response.get("status").getAsString().equals("EMAIL_CHANGE_NOT_ALLOWED_ERROR")); + assert (response.get("reason").getAsString().equals("Email already associated with another primary user.")); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/totp/api/TotpUserIdMappingTest.java b/src/test/java/io/supertokens/test/totp/api/TotpUserIdMappingTest.java index bc15b9ef4..7e135958c 100644 --- a/src/test/java/io/supertokens/test/totp/api/TotpUserIdMappingTest.java +++ b/src/test/java/io/supertokens/test/totp/api/TotpUserIdMappingTest.java @@ -4,9 +4,8 @@ import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.featureflag.EE_FEATURES; -import io.supertokens.featureflag.FeatureFlag; import io.supertokens.featureflag.FeatureFlagTestContent; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.totp.TOTPDevice; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.storageLayer.StorageLayer; @@ -14,7 +13,6 @@ import io.supertokens.test.Utils; import io.supertokens.test.httpRequest.HttpRequestForTesting; import io.supertokens.test.totp.TOTPRecipeTest; -import io.supertokens.test.totp.TotpLicenseTest; import io.supertokens.useridmapping.UserIdMapping; import static org.junit.Assert.assertNotNull; @@ -54,8 +52,8 @@ public void testExternalUserIdTranslation() throws Exception { JsonObject body = new JsonObject(); - UserInfo user = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = user.id; + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = user.getSupertokensUserId(); String externalUserId = "external-user-id"; // Create user id mapping first: diff --git a/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingStorageTest.java b/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingStorageTest.java index dee0ff75f..26e5bf129 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingStorageTest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingStorageTest.java @@ -20,7 +20,7 @@ import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage; @@ -96,19 +96,19 @@ public void testCreatingUserIdMapping() throws Exception { UserIdMappingStorage storage = (UserIdMappingStorage) StorageLayer.getStorage(process.main); // create a user - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPassword"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPassword"); String externalUserId = "external-test"; String externalUserIdInfo = "external-info"; // create a userId mapping - storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.id, externalUserId, + storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.getSupertokensUserId(), externalUserId, externalUserIdInfo); // check that the mapping exists - UserIdMapping userIdMapping = storage.getUserIdMapping(new AppIdentifier(null, null), userInfo.id, + UserIdMapping userIdMapping = storage.getUserIdMapping(new AppIdentifier(null, null), userInfo.getSupertokensUserId(), true); - assertEquals(userInfo.id, userIdMapping.superTokensUserId); + assertEquals(userInfo.getSupertokensUserId(), userIdMapping.superTokensUserId); assertEquals(externalUserId, userIdMapping.externalUserId); assertEquals(externalUserIdInfo, userIdMapping.externalUserIdInfo); @@ -129,16 +129,16 @@ public void testDuplicateUserIdMapping() throws Exception { UserIdMappingStorage storage = (UserIdMappingStorage) StorageLayer.getStorage(process.main); // create a user - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPassword"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPassword"); String externalUserId = "external-test"; - storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.id, externalUserId, null); + storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.getSupertokensUserId(), externalUserId, null); { // duplicate exception with both supertokensUserId and externalUserId Exception error = null; try { - storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.id, externalUserId, null); + storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.getSupertokensUserId(), externalUserId, null); } catch (Exception e) { error = e; } @@ -155,7 +155,7 @@ public void testDuplicateUserIdMapping() throws Exception { // duplicate exception with superTokensUserId Exception error = null; try { - storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.id, "newExternalId", null); + storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.getSupertokensUserId(), "newExternalId", null); } catch (Exception e) { error = e; } @@ -172,10 +172,10 @@ public void testDuplicateUserIdMapping() throws Exception { { // duplicate exception with externalUserId - UserInfo newUser = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); + AuthRecipeUserInfo newUser = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); Exception error = null; try { - storage.createUserIdMapping(new AppIdentifier(null, null), newUser.id, externalUserId, null); + storage.createUserIdMapping(new AppIdentifier(null, null), newUser.getSupertokensUserId(), externalUserId, null); } catch (Exception e) { error = e; } @@ -206,11 +206,11 @@ public void testCreatingAMappingWithAnUnknownStUserIdAndAPreexistingExternalUser UserIdMappingStorage storage = (UserIdMappingStorage) StorageLayer.getStorage(process.main); // create a User - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); String externalUserId = "externalUserId"; // create a userId mapping - storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.id, externalUserId, null); + storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.getSupertokensUserId(), externalUserId, null); // create a new mapping with unknown superTokensUserId and existing externalUserId Exception error = null; @@ -279,22 +279,22 @@ public void testRetrievingUserIdMapping() throws Exception { UserIdMappingStorage storage = (UserIdMappingStorage) StorageLayer.getStorage(process.main); // create a user - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); String externalUserId = "externalUserId"; String externalUserIdInfo = "externalUserIdInfo"; // create the mapping - storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.id, externalUserId, + storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.getSupertokensUserId(), externalUserId, externalUserIdInfo); // check that the mapping exists with supertokensUserId { - UserIdMapping userIdMapping = storage.getUserIdMapping(new AppIdentifier(null, null), userInfo.id, + UserIdMapping userIdMapping = storage.getUserIdMapping(new AppIdentifier(null, null), userInfo.getSupertokensUserId(), true); assertNotNull(userIdMapping); - assertEquals(userInfo.id, userIdMapping.superTokensUserId); + assertEquals(userInfo.getSupertokensUserId(), userIdMapping.superTokensUserId); assertEquals(externalUserId, userIdMapping.externalUserId); assertEquals(externalUserIdInfo, userIdMapping.externalUserIdInfo); } @@ -305,7 +305,7 @@ public void testRetrievingUserIdMapping() throws Exception { externalUserId, false); assertNotNull(userIdMapping); - assertEquals(userInfo.id, userIdMapping.superTokensUserId); + assertEquals(userInfo.getSupertokensUserId(), userIdMapping.superTokensUserId); assertEquals(externalUserId, userIdMapping.externalUserId); assertEquals(externalUserIdInfo, userIdMapping.externalUserIdInfo); } @@ -313,9 +313,9 @@ public void testRetrievingUserIdMapping() throws Exception { // check that the mapping exists with either { UserIdMapping[] userIdMappings = storage.getUserIdMapping(new AppIdentifier(null, null), - userInfo.id); + userInfo.getSupertokensUserId()); assertEquals(1, userIdMappings.length); - assertEquals(userInfo.id, userIdMappings[0].superTokensUserId); + assertEquals(userInfo.getSupertokensUserId(), userIdMappings[0].superTokensUserId); assertEquals(externalUserId, userIdMappings[0].externalUserId); assertEquals(externalUserIdInfo, userIdMappings[0].externalUserIdInfo); } @@ -323,7 +323,7 @@ public void testRetrievingUserIdMapping() throws Exception { UserIdMapping[] userIdMappings = storage.getUserIdMapping(new AppIdentifier(null, null), externalUserId); assertEquals(1, userIdMappings.length); - assertEquals(userInfo.id, userIdMappings[0].superTokensUserId); + assertEquals(userInfo.getSupertokensUserId(), userIdMappings[0].superTokensUserId); assertEquals(externalUserId, userIdMappings[0].externalUserId); assertEquals(externalUserIdInfo, userIdMappings[0].externalUserIdInfo); @@ -333,10 +333,10 @@ public void testRetrievingUserIdMapping() throws Exception { // superTokensUserId { - UserInfo newUserInfo = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); - String externalUserId2 = userInfo.id; + AuthRecipeUserInfo newUserInfo = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); + String externalUserId2 = userInfo.getSupertokensUserId(); - storage.createUserIdMapping(new AppIdentifier(null, null), newUserInfo.id, externalUserId2, null); + storage.createUserIdMapping(new AppIdentifier(null, null), newUserInfo.getSupertokensUserId(), externalUserId2, null); UserIdMapping[] userIdMappings = storage.getUserIdMapping(new AppIdentifier(null, null), externalUserId2); @@ -346,13 +346,13 @@ public void testRetrievingUserIdMapping() throws Exception { boolean checkThatUser2MappingIsReturned = false; for (UserIdMapping userIdMapping : userIdMappings) { - if (userIdMapping.superTokensUserId.equals(userInfo.id)) { - assertEquals(userInfo.id, userIdMapping.superTokensUserId); + if (userIdMapping.superTokensUserId.equals(userInfo.getSupertokensUserId())) { + assertEquals(userInfo.getSupertokensUserId(), userIdMapping.superTokensUserId); assertEquals(externalUserId, userIdMapping.externalUserId); assertEquals(externalUserIdInfo, userIdMapping.externalUserIdInfo); checkThatUser1MappingIsReturned = true; } else { - assertEquals(newUserInfo.id, userIdMapping.superTokensUserId); + assertEquals(newUserInfo.getSupertokensUserId(), userIdMapping.superTokensUserId); assertEquals(externalUserId2, userIdMapping.externalUserId); assertNull(userIdMapping.externalUserIdInfo); checkThatUser2MappingIsReturned = true; @@ -398,8 +398,8 @@ public void testDeletingAUserIdMapping() throws Exception { UserIdMappingStorage storage = (UserIdMappingStorage) StorageLayer.getStorage(process.main); // create a user - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalUserId"; { // create a new userId mapping @@ -492,9 +492,9 @@ public void testUpdatingExternalUserIdInfo() throws Exception { UserIdMappingStorage storage = (UserIdMappingStorage) StorageLayer.getStorage(process.main); // create User - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId"; String externalUserIdInfo = "externalUserIdInfo"; @@ -573,9 +573,9 @@ public void createUsersMapTheirIdsCheckRetrieveUseIdMappingsWithListOfUserIds() // create users equal to the User Pagination limit for (int i = 1; i <= AuthRecipe.USER_PAGINATION_LIMIT; i++) { - UserInfo userInfo = EmailPassword.signUp(process.main, "test" + i + "@example.com", "testPass123"); - superTokensUserIdList.add(userInfo.id); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test" + i + "@example.com", "testPass123"); + superTokensUserIdList.add(userInfo.getSupertokensUserId()); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId" + i; externalUserIdList.add(externalUserId); @@ -627,8 +627,8 @@ public void testCallingGetUserIdMappingForSuperTokensIdsWhenNoMappingExists() th ArrayList superTokensUserIdList = new ArrayList<>(); for (int i = 1; i <= 10; i++) { - UserInfo userInfo = EmailPassword.signUp(process.main, "test" + i + "@example.com", "testPass123"); - superTokensUserIdList.add(userInfo.id); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test" + i + "@example.com", "testPass123"); + superTokensUserIdList.add(userInfo.getSupertokensUserId()); } HashMap userIdMapping = storage.getUserIdMappingForSuperTokensIds(superTokensUserIdList); @@ -654,16 +654,16 @@ public void create10UsersAndMap5UsersIds() throws Exception { // create users equal to the User Pagination limit for (int i = 1; i <= 10; i++) { - UserInfo userInfo = EmailPassword.signUp(process.main, "test" + i + "@example.com", "testPass123"); - superTokensUserIdList.add(userInfo.id); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test" + i + "@example.com", "testPass123"); + superTokensUserIdList.add(userInfo.getSupertokensUserId()); if (i <= 5) { - userIdList.add(userInfo.id); + userIdList.add(userInfo.getSupertokensUserId()); } else { // create userIdMapping for the last 5 users String externalUserId = "externalId" + i; userIdList.add(externalUserId); - storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.id, externalUserId, null); + storage.createUserIdMapping(new AppIdentifier(null, null), userInfo.getSupertokensUserId(), externalUserId, null); } } diff --git a/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingTest.java b/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingTest.java index 1c44f621d..9a6cfb33a 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingTest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingTest.java @@ -24,7 +24,7 @@ import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.pluginInterface.ActiveUsersStorage; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; @@ -103,17 +103,17 @@ public void testDuplicateUserIdMapping() throws Exception { } // create a user - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPassword"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPassword"); String externalUserId = "external-test"; - UserIdMapping.createUserIdMapping(process.main, userInfo.id, externalUserId, null, false); + UserIdMapping.createUserIdMapping(process.main, userInfo.getSupertokensUserId(), externalUserId, null, false); { // duplicate exception with both supertokensUserId and externalUserId Exception error = null; try { - UserIdMapping.createUserIdMapping(process.main, userInfo.id, externalUserId, null, false); + UserIdMapping.createUserIdMapping(process.main, userInfo.getSupertokensUserId(), externalUserId, null, false); } catch (Exception e) { error = e; } @@ -130,7 +130,7 @@ public void testDuplicateUserIdMapping() throws Exception { // duplicate exception with superTokensUserId Exception error = null; try { - UserIdMapping.createUserIdMapping(process.main, userInfo.id, "newExternalId", null, false); + UserIdMapping.createUserIdMapping(process.main, userInfo.getSupertokensUserId(), "newExternalId", null, false); } catch (Exception e) { error = e; } @@ -147,10 +147,10 @@ public void testDuplicateUserIdMapping() throws Exception { { // duplicate exception with externalUserId - UserInfo newUser = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); + AuthRecipeUserInfo newUser = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); Exception error = null; try { - UserIdMapping.createUserIdMapping(process.main, newUser.id, externalUserId, null, false); + UserIdMapping.createUserIdMapping(process.main, newUser.getSupertokensUserId(), externalUserId, null, false); } catch (Exception e) { error = e; } @@ -181,19 +181,19 @@ public void testCreatingUserIdMapping() throws Exception { UserIdMappingStorage storage = (UserIdMappingStorage) StorageLayer.getStorage(process.main); // create a user - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPassword"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPassword"); String externalUserId = "external-test"; String externalUserIdInfo = "external-info"; // create a userId mapping - UserIdMapping.createUserIdMapping(process.getProcess(), userInfo.id, externalUserId, externalUserIdInfo, false); + UserIdMapping.createUserIdMapping(process.getProcess(), userInfo.getSupertokensUserId(), externalUserId, externalUserIdInfo, false); // check that the mapping exists io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = storage.getUserIdMapping( - new AppIdentifier(null, null), userInfo.id, + new AppIdentifier(null, null), userInfo.getSupertokensUserId(), true); - assertEquals(userInfo.id, userIdMapping.superTokensUserId); + assertEquals(userInfo.getSupertokensUserId(), userIdMapping.superTokensUserId); assertEquals(externalUserId, userIdMapping.externalUserId); assertEquals(externalUserIdInfo, userIdMapping.externalUserIdInfo); @@ -235,8 +235,8 @@ public void testRetrievingUserIdMapping() throws Exception { } // create a User and then a UserId mapping - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId"; String externalUserIdInfo = "externalIdInfo"; @@ -292,9 +292,9 @@ public void testRetrievingUserIdMapping() throws Exception { } // create a new mapping where the superTokensUserId of Mapping1 = externalUserId of Mapping2 - UserInfo userInfo2 = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); - String newSuperTokensUserId = userInfo2.id; - String newExternalUserId = userInfo.id; + AuthRecipeUserInfo userInfo2 = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); + String newSuperTokensUserId = userInfo2.getSupertokensUserId(); + String newExternalUserId = userInfo.getSupertokensUserId(); String newExternalUserIdInfo = "newExternalUserIdInfo"; UserIdMapping.createUserIdMapping(process.main, newSuperTokensUserId, newExternalUserId, newExternalUserIdInfo, @@ -369,8 +369,8 @@ public void testDeletingUserIdMapping() throws Exception { } // create mapping and check that it exists - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId"; String externalUserIdInfo = "externalIdInfo"; @@ -475,10 +475,10 @@ public void testDeletingUserIdMappingWithSharedId() throws Exception { // Create UserId mapping 1 - UserInfo userInfo_1 = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo userInfo_1 = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping_1 = new io.supertokens.pluginInterface.useridmapping.UserIdMapping( - userInfo_1.id, "externalUserId", "externalUserIdInfo"); + userInfo_1.getSupertokensUserId(), "externalUserId", "externalUserIdInfo"); // create the mapping and check that it exists { @@ -491,10 +491,10 @@ public void testDeletingUserIdMappingWithSharedId() throws Exception { // Create UserId mapping 2 - UserInfo userInfo_2 = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); + AuthRecipeUserInfo userInfo_2 = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping_2 = new io.supertokens.pluginInterface.useridmapping.UserIdMapping( - userInfo_2.id, userIdMapping_1.superTokensUserId, "externalUserIdInfo2"); + userInfo_2.getSupertokensUserId(), userIdMapping_1.superTokensUserId, "externalUserIdInfo2"); // create the mapping and check that it exists { @@ -570,9 +570,9 @@ public void testUpdatingExternalUserIdInfo() throws Exception { } // create User - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId"; // create a userId mapping @@ -665,9 +665,9 @@ public void testUpdatingExternalUserIdInfoWithSharedUserIds() throws Exception { // create two UserMappings where superTokensUserId in Mapping 1 = externalUserId in Mapping 2 // Create mapping 1 - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId"; String externalUserIdInfo = "externalUserIdInfo"; @@ -684,9 +684,9 @@ public void testUpdatingExternalUserIdInfoWithSharedUserIds() throws Exception { } // Create mapping 2 - UserInfo userInfo2 = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); - String superTokensUserId2 = userInfo2.id; - String externalUserId2 = userInfo.id; + AuthRecipeUserInfo userInfo2 = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); + String superTokensUserId2 = userInfo2.getSupertokensUserId(); + String externalUserId2 = userInfo.getSupertokensUserId(); String externalUserIdInfo2 = "newExternalUserIdInfo"; UserIdMapping.createUserIdMapping(process.main, superTokensUserId2, externalUserId2, externalUserIdInfo2, true); @@ -750,8 +750,8 @@ public void testUpdatingTheExternalUserIdInfoOfAMappingWithTheSameValue() throws // create a userIdMapping with externalUserIdInfo as null and update it to null { - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalUserId"; // create mapping @@ -763,8 +763,8 @@ public void testUpdatingTheExternalUserIdInfoOfAMappingWithTheSameValue() throws } { - UserInfo userInfo = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "newExternalUserIdInfo"; String externalUserIdInfo = "externalUserIdInfo"; @@ -808,8 +808,8 @@ public void checkThatCreateUserIdMappingHasAllNonAuthRecipeChecks() throws Excep } for (String className : classNames) { - UserInfo user = EmailPassword.signUp(process.main, "test@example.com", "password"); - String userId = user.id; + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "test@example.com", "password"); + String userId = user.getSupertokensUserId(); // create entry in nonAuth table StorageLayer.getStorage(process.main).addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier.BASE_TENANT, className, userId); @@ -887,10 +887,10 @@ public void checkThatDeleteUserIdMappingHasAllNonAuthRecipeChecks() throws Excep String externalId = "externalId"; for (String className : classNames) { // Create a User - UserInfo user = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); // create a mapping with the user - UserIdMapping.createUserIdMapping(process.main, user.id, externalId, null, false); + UserIdMapping.createUserIdMapping(process.main, user.getSupertokensUserId(), externalId, null, false); // create entry in nonAuth table with externalId StorageLayer.getStorage(process.main).addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier.BASE_TENANT, className, externalId); @@ -898,7 +898,7 @@ public void checkThatDeleteUserIdMappingHasAllNonAuthRecipeChecks() throws Excep // try to delete UserIdMapping String errorMessage = null; try { - UserIdMapping.deleteUserIdMapping(process.main, user.id, UserIdType.SUPERTOKENS, false); + UserIdMapping.deleteUserIdMapping(process.main, user.getSupertokensUserId(), UserIdType.SUPERTOKENS, false); } catch (ServletException e) { errorMessage = e.getRootCause().getMessage(); } @@ -907,7 +907,7 @@ public void checkThatDeleteUserIdMappingHasAllNonAuthRecipeChecks() throws Excep assertTrue(errorMessage.contains("UserId is already in use")); } // delete user data - AuthRecipe.deleteUser(process.main, user.id); + AuthRecipe.deleteUser(process.main, user.getSupertokensUserId()); } process.kill(); @@ -927,9 +927,9 @@ public void checkThatWeDontAllowDBStateA5FromBeingCreatedWhenForceIsFalse() thro } // create an EmailPassword User - UserInfo user_1 = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo user_1 = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); // create a mapping for the EmailPassword User - UserIdMapping.createUserIdMapping(process.main, user_1.id, "externalId", null, false); + UserIdMapping.createUserIdMapping(process.main, user_1.getSupertokensUserId(), "externalId", null, false); // create some metadata for the user JsonObject data = new JsonObject(); @@ -937,12 +937,12 @@ public void checkThatWeDontAllowDBStateA5FromBeingCreatedWhenForceIsFalse() thro UserMetadata.updateUserMetadata(process.main, "externalId", data); // Create another User - UserInfo user_2 = EmailPassword.signUp(process.main, "test123@example.com", "testPass123"); + AuthRecipeUserInfo user_2 = EmailPassword.signUp(process.main, "test123@example.com", "testPass123"); // try and map user_2 to user_1s superTokensUserId String errorMessage = null; try { - UserIdMapping.createUserIdMapping(process.main, user_2.id, user_1.id, null, false); + UserIdMapping.createUserIdMapping(process.main, user_2.getSupertokensUserId(), user_1.getSupertokensUserId(), null, false); } catch (ServletException e) { errorMessage = e.getRootCause().getMessage(); } @@ -964,18 +964,18 @@ public void testThatWeDontAllowDBStateA6WithoutForce() throws Exception { } // create user 1 - UserInfo user_1 = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo user_1 = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); // create user 2 - UserInfo user_2 = EmailPassword.signUp(process.main, "test123@example.com", "testPass123"); + AuthRecipeUserInfo user_2 = EmailPassword.signUp(process.main, "test123@example.com", "testPass123"); // create a mapping between User_1 and User_2 with force - UserIdMapping.createUserIdMapping(process.main, user_1.id, user_2.id, null, true); + UserIdMapping.createUserIdMapping(process.main, user_1.getSupertokensUserId(), user_2.getSupertokensUserId(), null, true); // try and create a mapping between User_2 and User_1 without force String errorMessage = null; try { - UserIdMapping.createUserIdMapping(process.main, user_2.id, user_1.id, null, false); + UserIdMapping.createUserIdMapping(process.main, user_2.getSupertokensUserId(), user_1.getSupertokensUserId(), null, false); } catch (ServletException e) { errorMessage = e.getRootCause().getMessage(); } @@ -1002,28 +1002,28 @@ public void testDeleteMappingWithUser_1AndUserIdTypeAsAny() throws Exception { } // create User_1 and User_2 - UserInfo user_1 = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - UserInfo user_2 = EmailPassword.signUp(process.main, "test123@exmaple.com", "testPass123"); + AuthRecipeUserInfo user_1 = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo user_2 = EmailPassword.signUp(process.main, "test123@exmaple.com", "testPass123"); // create a mapping between User_2 and User_1 with force - UserIdMapping.createUserIdMapping(process.main, user_2.id, user_1.id, null, true); + UserIdMapping.createUserIdMapping(process.main, user_2.getSupertokensUserId(), user_1.getSupertokensUserId(), null, true); // check that mapping exists { io.supertokens.pluginInterface.useridmapping.UserIdMapping mapping = UserIdMapping - .getUserIdMapping(process.main, user_2.id, UserIdType.SUPERTOKENS); + .getUserIdMapping(process.main, user_2.getSupertokensUserId(), UserIdType.SUPERTOKENS); assertNotNull(mapping); - assertEquals(mapping.superTokensUserId, user_2.id); - assertEquals(mapping.externalUserId, user_1.id); + assertEquals(mapping.superTokensUserId, user_2.getSupertokensUserId()); + assertEquals(mapping.externalUserId, user_1.getSupertokensUserId()); } // delete mapping with User_1s Id and UserIdType set to ANY, it should delete the mapping - assertTrue(UserIdMapping.deleteUserIdMapping(process.main, user_1.id, UserIdType.ANY, false)); + assertTrue(UserIdMapping.deleteUserIdMapping(process.main, user_1.getSupertokensUserId(), UserIdType.ANY, false)); // check that mapping is deleted { io.supertokens.pluginInterface.useridmapping.UserIdMapping mapping = UserIdMapping - .getUserIdMapping(process.main, user_2.id, UserIdType.SUPERTOKENS); + .getUserIdMapping(process.main, user_2.getSupertokensUserId(), UserIdType.SUPERTOKENS); assertNull(mapping); } @@ -1046,28 +1046,28 @@ public void testDeleteMappingWithUser_1AndUserIdTypeAsSUPERTOKENS() throws Excep } // create User_1 and User_2 - UserInfo user_1 = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - UserInfo user_2 = EmailPassword.signUp(process.main, "test123@exmaple.com", "testPass123"); + AuthRecipeUserInfo user_1 = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo user_2 = EmailPassword.signUp(process.main, "test123@exmaple.com", "testPass123"); // create a mapping between User_2 and User_1 with force - UserIdMapping.createUserIdMapping(process.main, user_2.id, user_1.id, null, true); + UserIdMapping.createUserIdMapping(process.main, user_2.getSupertokensUserId(), user_1.getSupertokensUserId(), null, true); // check that mapping exists { io.supertokens.pluginInterface.useridmapping.UserIdMapping mapping = UserIdMapping - .getUserIdMapping(process.main, user_2.id, UserIdType.SUPERTOKENS); + .getUserIdMapping(process.main, user_2.getSupertokensUserId(), UserIdType.SUPERTOKENS); assertNotNull(mapping); - assertEquals(mapping.superTokensUserId, user_2.id); - assertEquals(mapping.externalUserId, user_1.id); + assertEquals(mapping.superTokensUserId, user_2.getSupertokensUserId()); + assertEquals(mapping.externalUserId, user_1.getSupertokensUserId()); } // delete mapping with User_1s Id and UserIdType set to ANY, it should delete the mapping - assertTrue(UserIdMapping.deleteUserIdMapping(process.main, user_1.id, UserIdType.SUPERTOKENS, false)); + assertTrue(UserIdMapping.deleteUserIdMapping(process.main, user_1.getSupertokensUserId(), UserIdType.SUPERTOKENS, false)); // check that mapping is deleted { io.supertokens.pluginInterface.useridmapping.UserIdMapping mapping = UserIdMapping - .getUserIdMapping(process.main, user_2.id, UserIdType.SUPERTOKENS); + .getUserIdMapping(process.main, user_2.getSupertokensUserId(), UserIdType.SUPERTOKENS); assertNull(mapping); } diff --git a/src/test/java/io/supertokens/test/userIdMapping/api/CreateUserIdMappingAPITest.java b/src/test/java/io/supertokens/test/userIdMapping/api/CreateUserIdMappingAPITest.java index 2c1a8c61e..8cc8d4e16 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/api/CreateUserIdMappingAPITest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/api/CreateUserIdMappingAPITest.java @@ -21,7 +21,7 @@ import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage; @@ -32,9 +32,6 @@ import io.supertokens.test.httpRequest.HttpResponseException; import io.supertokens.usermetadata.UserMetadata; import io.supertokens.utils.SemVer; -import io.supertokens.userroles.UserRoles; -import io.supertokens.utils.SemVer; -import io.supertokens.webserver.WebserverAPI; import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; @@ -227,13 +224,13 @@ public void testCreatingAUserIdMappingWithAndWithoutForce() throws Exception { } // create a User and add some non auth recipe info - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); // add some metadata to the user JsonObject userMetadata = new JsonObject(); userMetadata.addProperty("test", "testExample"); - UserMetadata.updateUserMetadata(process.main, userInfo.id, userMetadata); - String superTokensUserId = userInfo.id; + UserMetadata.updateUserMetadata(process.main, userInfo.getSupertokensUserId(), userMetadata); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId"; // try and create mapping without force @@ -284,8 +281,8 @@ public void testCreatingAUserIdMapping() throws Exception { UserIdMappingStorage storage = (UserIdMappingStorage) StorageLayer.getStorage(process.main); // create a User - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "userId"; String externalUserIdInfo = "externUserIdInfo"; @@ -356,10 +353,10 @@ public void testCreatingUserIdMappingWithExternalUserIdInfoAsNull() throws Excep return; } - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); String externalUserId = "externalUserId"; JsonObject requestBody = new JsonObject(); - requestBody.addProperty("superTokensUserId", userInfo.id); + requestBody.addProperty("superTokensUserId", userInfo.getSupertokensUserId()); requestBody.addProperty("externalUserId", externalUserId); requestBody.add("externalUserIdInfo", null); @@ -369,11 +366,11 @@ public void testCreatingUserIdMappingWithExternalUserIdInfoAsNull() throws Excep UserIdMappingStorage storage = (UserIdMappingStorage) StorageLayer.getStorage(process.main); - UserIdMapping userIdMapping = storage.getUserIdMapping(new AppIdentifier(null, null), userInfo.id, + UserIdMapping userIdMapping = storage.getUserIdMapping(new AppIdentifier(null, null), userInfo.getSupertokensUserId(), true); assertNotNull(userIdMapping); - assertEquals(userInfo.id, userIdMapping.superTokensUserId); + assertEquals(userInfo.getSupertokensUserId(), userIdMapping.superTokensUserId); assertEquals(externalUserId, userIdMapping.externalUserId); assertNull(userIdMapping.externalUserIdInfo); @@ -393,9 +390,9 @@ public void testCreatingDuplicateUserIdMapping() throws Exception { return; } - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalUserId"; // create UserId mapping @@ -439,11 +436,11 @@ public void testCreatingDuplicateUserIdMapping() throws Exception { { // create a duplicate mapping with externalUserId - UserInfo newUserInfo = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); + AuthRecipeUserInfo newUserInfo = EmailPassword.signUp(process.main, "test2@example.com", "testPass123"); JsonObject requestBody = new JsonObject(); - requestBody.addProperty("superTokensUserId", newUserInfo.id); + requestBody.addProperty("superTokensUserId", newUserInfo.getSupertokensUserId()); requestBody.addProperty("externalUserId", externalUserId); JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", diff --git a/src/test/java/io/supertokens/test/userIdMapping/api/GetUserIdMappingAPITest.java b/src/test/java/io/supertokens/test/userIdMapping/api/GetUserIdMappingAPITest.java index a9d437ede..4544013c8 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/api/GetUserIdMappingAPITest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/api/GetUserIdMappingAPITest.java @@ -20,7 +20,7 @@ import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -196,8 +196,8 @@ public void testRetrieveUserIdMapping() throws Exception { } // create a user and map their userId to an external userId - UserInfo user = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = user.id; + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = user.getSupertokensUserId(); String externalUserId = "externalUserId"; String externalUserIdInfo = "externalUserIdInfo"; @@ -290,8 +290,8 @@ public void testRetrievingUserIdMappingWithoutSendingUserIdType() throws Excepti } // create a user and map their userId to an external userId - UserInfo user = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = user.id; + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = user.getSupertokensUserId(); String externalUserId = "externalUserId"; String externalUserIdInfo = "externalUserIdInfo"; @@ -345,8 +345,8 @@ public void testRetrieveUserIdMappingWithExternalUserIdInfoAsNull() throws Excep } // create a user and map their userId to an external userId - UserInfo user = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = user.id; + AuthRecipeUserInfo user = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = user.getSupertokensUserId(); String externalUserId = "externalUserId"; UserIdMapping.createUserIdMapping(process.main, superTokensUserId, externalUserId, null, false); diff --git a/src/test/java/io/supertokens/test/userIdMapping/api/RemoveUserIdMappingAPITest.java b/src/test/java/io/supertokens/test/userIdMapping/api/RemoveUserIdMappingAPITest.java index 0ec895d3e..225ba319f 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/api/RemoveUserIdMappingAPITest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/api/RemoveUserIdMappingAPITest.java @@ -17,12 +17,10 @@ package io.supertokens.test.userIdMapping.api; import com.google.gson.JsonObject; -import io.supertokens.Main; import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; -import io.supertokens.emailverification.User; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; @@ -38,8 +36,6 @@ import org.junit.Test; import org.junit.rules.TestRule; -import jakarta.servlet.ServletException; - import static io.supertokens.test.Utils.createUserIdMappingAndCheckThatItExists; import static org.junit.Assert.*; @@ -234,8 +230,8 @@ public void testDeletingUserIdMapping() throws Exception { } // create a userId mapping - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - UserIdMapping userIdMapping = new UserIdMapping(userInfo.id, "externalUserId", "externalUserIdInfo"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + UserIdMapping userIdMapping = new UserIdMapping(userInfo.getSupertokensUserId(), "externalUserId", "externalUserIdInfo"); createUserIdMappingAndCheckThatItExists(process.main, userIdMapping); // delete userId mapping with userIdType as SUPERTOKENS @@ -336,8 +332,8 @@ public void testDeletingAUserIdMappingWithoutSendingUserIdType() throws Exceptio } // create a userId mapping - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - UserIdMapping userIdMapping = new UserIdMapping(userInfo.id, "externalUserId", "externalUserIdInfo"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + UserIdMapping userIdMapping = new UserIdMapping(userInfo.getSupertokensUserId(), "externalUserId", "externalUserIdInfo"); createUserIdMappingAndCheckThatItExists(process.main, userIdMapping); { @@ -393,8 +389,8 @@ public void deleteUserIdMappingWithAndWithoutForce() throws Exception { return; } - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalId = "externalId"; io.supertokens.useridmapping.UserIdMapping.createUserIdMapping(process.main, superTokensUserId, externalId, null, false); diff --git a/src/test/java/io/supertokens/test/userIdMapping/api/UpdateExternalUserIdInfoTest.java b/src/test/java/io/supertokens/test/userIdMapping/api/UpdateExternalUserIdInfoTest.java index b9472bbb6..fe3f930cd 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/api/UpdateExternalUserIdInfoTest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/api/UpdateExternalUserIdInfoTest.java @@ -20,7 +20,7 @@ import io.supertokens.ProcessState; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; @@ -269,8 +269,8 @@ public void testUpdatingExternalUserIdInfoWithSuperTokensUserId() throws Excepti // create userId mapping with externalUserIdInfo String externalUserIdInfo = "externalUserIdInfo"; - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - UserIdMapping userIdMapping = new io.supertokens.pluginInterface.useridmapping.UserIdMapping(userInfo.id, + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + UserIdMapping userIdMapping = new io.supertokens.pluginInterface.useridmapping.UserIdMapping(userInfo.getSupertokensUserId(), "externalUserIdInfo", externalUserIdInfo); Utils.createUserIdMappingAndCheckThatItExists(process.main, userIdMapping); @@ -340,8 +340,8 @@ public void testUpdatingExternalUserIdInfoWithExternalUserId() throws Exception // create userId mapping with externalUserIdInfo String externalUserIdInfo = "externalUserIdInfo"; - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - UserIdMapping userIdMapping = new io.supertokens.pluginInterface.useridmapping.UserIdMapping(userInfo.id, + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + UserIdMapping userIdMapping = new io.supertokens.pluginInterface.useridmapping.UserIdMapping(userInfo.getSupertokensUserId(), "externalUserIdInfo", externalUserIdInfo); Utils.createUserIdMappingAndCheckThatItExists(process.main, userIdMapping); @@ -411,8 +411,8 @@ public void testDeletingExternalUserIdInfo() throws Exception { // create userId mapping with externalUserIdInfo String externalUserIdInfo = "externalUserIdInfo"; - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); - UserIdMapping userIdMapping = new io.supertokens.pluginInterface.useridmapping.UserIdMapping(userInfo.id, + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + UserIdMapping userIdMapping = new io.supertokens.pluginInterface.useridmapping.UserIdMapping(userInfo.getSupertokensUserId(), "externalUserIdInfo", externalUserIdInfo); Utils.createUserIdMappingAndCheckThatItExists(process.main, userIdMapping); diff --git a/src/test/java/io/supertokens/test/userIdMapping/recipe/EmailPasswordAPITest.java b/src/test/java/io/supertokens/test/userIdMapping/recipe/EmailPasswordAPITest.java index fd6342e1f..801d7bd6d 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/recipe/EmailPasswordAPITest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/recipe/EmailPasswordAPITest.java @@ -21,13 +21,11 @@ import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; -import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; import io.supertokens.test.httpRequest.HttpRequestForTesting; -import io.supertokens.test.httpRequest.HttpResponseException; import io.supertokens.useridmapping.UserIdMapping; import io.supertokens.useridmapping.UserIdType; import io.supertokens.utils.SemVer; @@ -57,7 +55,7 @@ public void beforeEach() { @Test public void testSignInAPI() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -69,8 +67,8 @@ public void testSignInAPI() throws Exception { // create a User String email = "test@example.com"; String password = "testPass123"; - UserInfo userInfo = EmailPassword.signUp(process.main, email, password); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, email, password); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId"; // create the mapping @@ -112,7 +110,7 @@ public void testSignInAPI() throws Exception { @Test public void testResetPasswordFlowWithUserIdMapping() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -124,8 +122,8 @@ public void testResetPasswordFlowWithUserIdMapping() throws Exception { // create a User String email = "test@example.com"; String password = "testPass123"; - UserInfo userInfo = EmailPassword.signUp(process.main, email, password); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, email, password); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId"; // create the mapping @@ -162,7 +160,7 @@ public void testResetPasswordFlowWithUserIdMapping() throws Exception { } // sign in with the new password and check that it works - UserInfo userInfo1 = EmailPassword.signIn(process.main, email, newPassword); + AuthRecipeUserInfo userInfo1 = EmailPassword.signIn(process.main, email, newPassword); assertNotNull(userInfo1); process.kill(); @@ -171,7 +169,7 @@ public void testResetPasswordFlowWithUserIdMapping() throws Exception { @Test public void testRetrievingUser() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -183,8 +181,8 @@ public void testRetrievingUser() throws Exception { // create a User String email = "test@example.com"; String password = "testPass123"; - UserInfo userInfo = EmailPassword.signUp(process.main, email, password); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, email, password); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId"; // create the mapping @@ -217,7 +215,7 @@ public void testRetrievingUser() throws Exception { @Test public void testUpdatingUsersEmail() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -229,8 +227,8 @@ public void testUpdatingUsersEmail() throws Exception { // create a User String email = "test@example.com"; String password = "testPass123"; - UserInfo userInfo = EmailPassword.signUp(process.main, email, password); - String superTokensUserId = userInfo.id; + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, email, password); + String superTokensUserId = userInfo.getSupertokensUserId(); String externalUserId = "externalId"; // create the mapping @@ -250,7 +248,7 @@ public void testUpdatingUsersEmail() throws Exception { } // check that you can now sign in with the new email - UserInfo userInfo1 = EmailPassword.signIn(process.main, newEmail, password); + AuthRecipeUserInfo userInfo1 = EmailPassword.signIn(process.main, newEmail, password); assertNotNull(userInfo1); process.kill(); diff --git a/src/test/java/io/supertokens/test/userIdMapping/recipe/PasswordlessAPITest.java b/src/test/java/io/supertokens/test/userIdMapping/recipe/PasswordlessAPITest.java index efdd23d1d..96f70ec97 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/recipe/PasswordlessAPITest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/recipe/PasswordlessAPITest.java @@ -16,14 +16,12 @@ package io.supertokens.test.userIdMapping.recipe; -import com.google.gson.JsonArray; -import com.google.gson.JsonNull; import com.google.gson.JsonObject; import io.supertokens.ProcessState; import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.passwordless.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -57,7 +55,7 @@ public void beforeEach() { @Test public void testCreatingAPasswordlessUserMapTheirUserIdAndRetrieveUserId() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -76,7 +74,7 @@ public void testCreatingAPasswordlessUserMapTheirUserIdAndRetrieveUserId() throw createCodeResponse.deviceId, createCodeResponse.deviceIdHash, createCodeResponse.userInputCode, null); assertTrue(consumeCodeResponse.createdNewUser); - superTokensUserId = consumeCodeResponse.user.id; + superTokensUserId = consumeCodeResponse.user.getSupertokensUserId(); // create mapping UserIdMapping.createUserIdMapping(process.main, superTokensUserId, externalId, null, false); @@ -123,7 +121,7 @@ public void testCreatingAPasswordlessUserMapTheirUserIdAndRetrieveUserId() throw @Test public void testCreatingAPasswordlessUserAndRetrieveInfo() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -143,7 +141,7 @@ public void testCreatingAPasswordlessUserAndRetrieveInfo() throws Exception { createCodeResponse.deviceId, createCodeResponse.deviceIdHash, createCodeResponse.userInputCode, null); assertTrue(consumeCodeResponse.createdNewUser); - superTokensUserId = consumeCodeResponse.user.id; + superTokensUserId = consumeCodeResponse.user.getSupertokensUserId(); // create mapping UserIdMapping.createUserIdMapping(process.main, superTokensUserId, externalId, null, false); @@ -201,7 +199,7 @@ public void testCreatingAPasswordlessUserAndRetrieveInfo() throws Exception { @Test public void testCreatingPasswordlessUserWithPhoneNumberAndRetrieveInfo() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -221,7 +219,7 @@ public void testCreatingPasswordlessUserWithPhoneNumberAndRetrieveInfo() throws createCodeResponse.deviceId, createCodeResponse.deviceIdHash, createCodeResponse.userInputCode, null); assertTrue(consumeCodeResponse.createdNewUser); - superTokensUserId = consumeCodeResponse.user.id; + superTokensUserId = consumeCodeResponse.user.getSupertokensUserId(); // create mapping UserIdMapping.createUserIdMapping(process.main, superTokensUserId, externalId, null, false); @@ -252,7 +250,7 @@ public void testCreatingPasswordlessUserWithPhoneNumberAndRetrieveInfo() throws @Test public void testUpdatingPasswordlessUserWithTheirExternalId() throws Exception { - String[] args = { "../" }; + String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -269,7 +267,7 @@ public void testUpdatingPasswordlessUserWithTheirExternalId() throws Exception { null); Passwordless.ConsumeCodeResponse consumeCodeResponse = Passwordless.consumeCode(process.main, response.deviceId, response.deviceIdHash, response.userInputCode, null); - superTokensUserId = consumeCodeResponse.user.id; + superTokensUserId = consumeCodeResponse.user.getSupertokensUserId(); // map their userId UserIdMapping.createUserIdMapping(process.main, superTokensUserId, externalId, null, false); @@ -287,9 +285,9 @@ public void testUpdatingPasswordlessUserWithTheirExternalId() throws Exception { assertEquals(updateUserResponse.get("status").getAsString(), "OK"); // check that user got updated - UserInfo userInfo = Passwordless.getUserByEmail(process.main, newEmail); + AuthRecipeUserInfo userInfo = Passwordless.getUserByEmail(process.main, newEmail); assertNotNull(userInfo); - assertEquals(userInfo.id, superTokensUserId); + assertEquals(userInfo.getSupertokensUserId(), superTokensUserId); } process.kill(); diff --git a/src/test/java/io/supertokens/test/userIdMapping/recipe/ThirdPartyAPITest.java b/src/test/java/io/supertokens/test/userIdMapping/recipe/ThirdPartyAPITest.java index 71f7e6c43..0f3d7d5c6 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/recipe/ThirdPartyAPITest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/recipe/ThirdPartyAPITest.java @@ -20,9 +20,7 @@ import com.google.gson.JsonObject; import io.supertokens.ProcessState; import io.supertokens.authRecipe.AuthRecipe; -import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.thirdparty.UserInfo; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -72,7 +70,7 @@ public void testSignInAPI() throws Exception { String email = "test@example.com"; ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, thirdPartyId, thirdPartyUserId, email); - String superTokensUserId = signInUpResponse.user.id; + String superTokensUserId = signInUpResponse.user.getSupertokensUserId(); String externalUserId = "externalId"; // create the mapping @@ -132,7 +130,7 @@ public void testGetUsersByEmailAPI() throws Exception { String email = "test@example.com"; ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, thirdPartyId, thirdPartyUserId, email); - String superTokensUserId = signInUpResponse.user.id; + String superTokensUserId = signInUpResponse.user.getSupertokensUserId(); String externalUserId = "externalId"; // create the mapping @@ -174,7 +172,7 @@ public void testGetUserById() throws Exception { String email = "test@example.com"; ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, thirdPartyId, thirdPartyUserId, email); - String superTokensUserId = signInUpResponse.user.id; + String superTokensUserId = signInUpResponse.user.getSupertokensUserId(); String externalUserId = "externalId"; // create the mapping @@ -215,7 +213,7 @@ public void testGetUserByThirdPartyId() throws Exception { String email = "test@example.com"; ThirdParty.SignInUpResponse signInUpResponse = ThirdParty.signInUp(process.main, thirdPartyId, thirdPartyUserId, email); - String superTokensUserId = signInUpResponse.user.id; + String superTokensUserId = signInUpResponse.user.getSupertokensUserId(); String externalUserId = "externalId"; // create the mapping diff --git a/src/test/java/io/supertokens/test/userRoles/UserRolesTest.java b/src/test/java/io/supertokens/test/userRoles/UserRolesTest.java index bcc3587f8..726001688 100644 --- a/src/test/java/io/supertokens/test/userRoles/UserRolesTest.java +++ b/src/test/java/io/supertokens/test/userRoles/UserRolesTest.java @@ -20,7 +20,7 @@ import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.pluginInterface.STORAGE_TYPE; -import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.userroles.exception.UnknownRoleException; @@ -1005,28 +1005,28 @@ public void createAnAuthUserAssignRolesAndDeleteUser() throws Exception { UserRoles.createNewRoleOrModifyItsPermissions(process.main, role, null); // Create an Auth User - UserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPassword"); + AuthRecipeUserInfo userInfo = EmailPassword.signUp(process.main, "test@example.com", "testPassword"); // assign role to user - UserRoles.addRoleToUser(process.main, userInfo.id, role); + UserRoles.addRoleToUser(process.main, userInfo.getSupertokensUserId(), role); { // check that user has role - String[] retrievedRoles = UserRoles.getRolesForUser(process.main, userInfo.id); + String[] retrievedRoles = UserRoles.getRolesForUser(process.main, userInfo.getSupertokensUserId()); assertEquals(1, retrievedRoles.length); assertEquals(role, retrievedRoles[0]); } // delete User - AuthRecipe.deleteUser(process.main, userInfo.id); + AuthRecipe.deleteUser(process.main, userInfo.getSupertokensUserId()); { // check that user has no roles - String[] retrievedRoles = UserRoles.getRolesForUser(process.main, userInfo.id); + String[] retrievedRoles = UserRoles.getRolesForUser(process.main, userInfo.getSupertokensUserId()); assertEquals(0, retrievedRoles.length); // check that the mapping for user role doesnt exist - String[] roleUserMapping = storage.getRolesForUser(new TenantIdentifier(null, null, null), userInfo.id); + String[] roleUserMapping = storage.getRolesForUser(new TenantIdentifier(null, null, null), userInfo.getSupertokensUserId()); assertEquals(0, roleUserMapping.length); }