diff --git a/CHANGELOG.md b/CHANGELOG.md index 24905724f..e3cf4db89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [9.0.0] - 2024-03-13 + +### Added + +- Supports CDI version `5.0` +- MFA stats in `EEFeatureFlag` +- Adds `ImportTotpDeviceAPI` + +### Changes + +- `deviceName` in request body of `CreateOrUpdateTotpDeviceAPI` `POST` is now optional +- Adds `firstFactors` and `requiredSecondaryFactors` in request body of create or update CUD, App and + Tenant APIs +- Adds `deviceName` in the response of `CreateOrUpdateTotpDeviceAPI` `POST` +- `VerifyTOTPAPI` changes + - Removes `allowUnverifiedDevices` from request body and unverified devices are not allowed + - Adds `currentNumberOfFailedAttempts` and `maxNumberOfFailedAttempts` in response when status is + `INVALID_TOTP_ERROR` or `LIMIT_REACHED_ERROR` + - Adds status `UNKNOWN_USER_ID_ERROR` +- `VerifyTotpDeviceAPI` changes + - Adds `currentNumberOfFailedAttempts` and `maxNumberOfFailedAttempts` in response when status is + `INVALID_TOTP_ERROR` or `LIMIT_REACHED_ERROR` +- Adds a new required `useDynamicSigningKey` into the request body of `RefreshSessionAPI` + - This enables smooth switching between `useDynamicAccessTokenSigningKey` settings by allowing refresh calls to + change the signing key type of a session + +### Migration + +- TODO - copy once postgres / mysql changelog is done + ## [8.0.1] - 2024-03-11 - Making this version backward compatible. Breaking changes in `8.0.0` can now be ignored. diff --git a/build.gradle b/build.gradle index fa4132534..6080bc20d 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" } // } //} -version = "8.0.1" +version = "9.0.0" repositories { diff --git a/coreDriverInterfaceSupported.json b/coreDriverInterfaceSupported.json index 0c9d09fe0..00fa393ac 100644 --- a/coreDriverInterfaceSupported.json +++ b/coreDriverInterfaceSupported.json @@ -17,6 +17,7 @@ "2.20", "2.21", "3.0", - "4.0" + "4.0", + "5.0" ] -} \ No newline at end of file +} diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 41c2dda5e..42d1185f3 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -185,43 +185,43 @@ private JsonObject getDashboardLoginStats() throws TenantOrAppNotFoundException, return stats; } - private JsonObject getTOTPStats() throws StorageQueryException, TenantOrAppNotFoundException { - JsonObject totpStats = new JsonObject(); - JsonArray totpMauArr = new JsonArray(); + private boolean isEnterpriseThirdPartyId(String thirdPartyId) { + for (String enterpriseThirdPartyId : ENTERPRISE_THIRD_PARTY_IDS) { + if (thirdPartyId.startsWith(enterpriseThirdPartyId)) { + return true; + } + } + return false; + } + private JsonObject getMFAStats() throws StorageQueryException, TenantOrAppNotFoundException { + // TODO: Active users are present only on public tenant and MFA users may be + // present on different storages + JsonObject result = new JsonObject(); Storage[] storages = StorageLayer.getStoragesForApp(main, this.appIdentifier); - // TODO Active users are present only on public tenant and TOTP users may be present on different storages - Storage publicTenantStorage = StorageLayer.getStorage(this.appIdentifier.getAsPublicTenantIdentifier(), main); - final long now = System.currentTimeMillis(); - for (int i = 1; i <= 31; i++) { - long timestamp = now - (i * 24 * 60 * 60 * 1000L); - - int totpMau = 0; - // TODO Need to figure out a way to combine the data from different storages to get the final stats - // for (Storage storage : storages) { - totpMau += ((ActiveUsersStorage) publicTenantStorage).countUsersEnabledTotpAndActiveSince(this.appIdentifier, timestamp); - // } - totpMauArr.add(new JsonPrimitive(totpMau)); - } + int totalUserCountWithMoreThanOneLoginMethod = 0; + int[] maus = new int[31]; - totpStats.add("maus", totpMauArr); + long now = System.currentTimeMillis(); - int totpTotalUsers = 0; for (Storage storage : storages) { - totpTotalUsers += ((ActiveUsersStorage) storage).countUsersEnabledTotp(this.appIdentifier); - } - totpStats.addProperty("total_users", totpTotalUsers); - return totpStats; - } + totalUserCountWithMoreThanOneLoginMethod += ((AuthRecipeStorage) storage) + .getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(this.appIdentifier); - private boolean isEnterpriseThirdPartyId(String thirdPartyId) { - for (String enterpriseThirdPartyId : ENTERPRISE_THIRD_PARTY_IDS) { - if (thirdPartyId.startsWith(enterpriseThirdPartyId)) { - return true; + for (int i = 1; i <= 31; i++) { + long timestamp = now - (i * 24 * 60 * 60 * 1000L); + + // `maus[i-1]` since i starts from 1 + maus[i - 1] += ((ActiveUsersStorage) storage) + .countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(appIdentifier, timestamp); } } - return false; + + result.addProperty("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled", + totalUserCountWithMoreThanOneLoginMethod); + result.add("mauWithMoreThanOneLoginMethodOrTOTPEnabled", new Gson().toJsonTree(maus)); + return result; } private JsonObject getMultiTenancyStats() @@ -245,6 +245,21 @@ private JsonObject getMultiTenancyStats() hasUsersOrSessions = hasUsersOrSessions || ((SessionSQLStorage) storage).getNumberOfSessions(tenantConfig.tenantIdentifier) > 0; tenantStat.addProperty("usersCount", usersCount); tenantStat.addProperty("hasUsersOrSessions", hasUsersOrSessions); + if (tenantConfig.firstFactors != null) { + JsonArray firstFactors = new JsonArray(); + for (String firstFactor : tenantConfig.firstFactors) { + firstFactors.add(new JsonPrimitive(firstFactor)); + } + tenantStat.add("firstFactors", firstFactors); + } + + if (tenantConfig.requiredSecondaryFactors != null) { + JsonArray requiredSecondaryFactors = new JsonArray(); + for (String requiredSecondaryFactor : tenantConfig.requiredSecondaryFactors) { + requiredSecondaryFactors.add(new JsonPrimitive(requiredSecondaryFactor)); + } + tenantStat.add("requiredSecondaryFactors", requiredSecondaryFactors); + } try { tenantStat.addProperty("userPoolId", Utils.hashSHA256(storage.getUserPoolId())); @@ -355,8 +370,8 @@ public JsonObject getPaidFeatureStats() throws StorageQueryException, TenantOrAp usageStats.add(EE_FEATURES.DASHBOARD_LOGIN.toString(), getDashboardLoginStats()); } - if (feature == EE_FEATURES.TOTP) { - usageStats.add(EE_FEATURES.TOTP.toString(), getTOTPStats()); + if (feature == EE_FEATURES.MFA) { + usageStats.add(EE_FEATURES.MFA.toString(), getMFAStats()); } if (feature == EE_FEATURES.MULTI_TENANCY) { diff --git a/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java b/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java index 189d2b1d1..6acc45566 100644 --- a/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java +++ b/ee/src/test/java/io/supertokens/ee/test/TestMultitenancyStats.java @@ -78,6 +78,7 @@ public void testPaidStatsIsSentForAllAppsInMultitenancy() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ), false); @@ -86,6 +87,7 @@ public void testPaidStatsIsSentForAllAppsInMultitenancy() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ), false); @@ -94,6 +96,7 @@ public void testPaidStatsIsSentForAllAppsInMultitenancy() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ), false); } diff --git a/jar/core-5.0.0.jar b/jar/core-5.0.0.jar new file mode 100644 index 000000000..90b001dbe Binary files /dev/null and b/jar/core-5.0.0.jar differ diff --git a/jar/core-8.0.0.jar b/jar/core-8.0.0.jar new file mode 100644 index 000000000..fc8067d57 Binary files /dev/null and b/jar/core-8.0.0.jar differ diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index e9d4c148c..f9d5be771 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": [ - "5.0" + "6.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 7ee3c5534..231b069ca 100644 --- a/src/main/java/io/supertokens/ActiveUsers.java +++ b/src/main/java/io/supertokens/ActiveUsers.java @@ -1,8 +1,8 @@ package io.supertokens; +import io.supertokens.pluginInterface.ActiveUsersSQLStorage; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.StorageUtils; -import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; @@ -37,6 +37,20 @@ public static int countUsersActiveSince(Main main, AppIdentifier appIdentifier, return StorageUtils.getActiveUsersStorage(storage).countUsersActiveSince(appIdentifier, time); } + public static void updateLastActiveAfterLinking(Main main, AppIdentifier appIdentifier, String primaryUserId, + String recipeUserId) + throws StorageQueryException, TenantOrAppNotFoundException, StorageTransactionLogicException { + ActiveUsersSQLStorage activeUsersStorage = + (ActiveUsersSQLStorage) StorageUtils.getActiveUsersStorage(StorageLayer.getStorage(appIdentifier.getAsPublicTenantIdentifier(), main)); + + activeUsersStorage.startTransaction(con -> { + activeUsersStorage.deleteUserActive_Transaction(con, appIdentifier, recipeUserId); + return null; + }); + + updateLastActive(appIdentifier, main, primaryUserId); + } + @TestOnly public static int countUsersActiveSince(Main main, long time) throws StorageQueryException, TenantOrAppNotFoundException { diff --git a/src/main/java/io/supertokens/Main.java b/src/main/java/io/supertokens/Main.java index 3620b2bf8..2998efb7b 100644 --- a/src/main/java/io/supertokens/Main.java +++ b/src/main/java/io/supertokens/Main.java @@ -157,6 +157,9 @@ private void init() throws IOException, StorageQueryException { throw new QuitProgramException(e); } + // loading version file + Version.loadVersion(this, CLIOptions.get(this).getInstallationPath() + "version.yaml"); + Logging.info(this, TenantIdentifier.BASE_TENANT, "Completed config.yaml loading.", true); // loading storage layer @@ -167,9 +170,6 @@ private void init() throws IOException, StorageQueryException { throw new QuitProgramException(e); } - // loading version file - Version.loadVersion(this, CLIOptions.get(this).getInstallationPath() + "version.yaml"); - // init file logging Logging.initFileLogging(this); diff --git a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java index e480f45f2..271b93d52 100644 --- a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java +++ b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java @@ -25,11 +25,7 @@ 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.StorageUtils; -import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage; +import io.supertokens.pluginInterface.*; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; @@ -126,7 +122,7 @@ public static AuthRecipeUserInfo getUserById(Main main, String userId) public static AuthRecipeUserInfo getUserById(AppIdentifier appIdentifier, Storage storage, String userId) throws StorageQueryException { - return StorageUtils.getAuthRecipeStorage(storage).getPrimaryUserById(appIdentifier, userId); + return StorageUtils.getAuthRecipeStorage(storage).getPrimaryUserById(appIdentifier, userId); } public static class CreatePrimaryUserResult { @@ -325,22 +321,22 @@ public static LinkAccountsResult linkAccounts(Main main, String recipeUserId, St } public static LinkAccountsResult linkAccounts(Main main, AppIdentifier appIdentifier, - Storage storage, String _recipeUserId, String _primaryUserId) + Storage storage, String _recipeUserId, String _primaryUserId) throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException, RecipeUserIdAlreadyLinkedWithAnotherPrimaryUserIdException, InputUserIdIsNotAPrimaryUserException, UnknownUserIdException, TenantOrAppNotFoundException, FeatureNotEnabledException { if (Arrays.stream(FeatureFlag.getInstance(main, appIdentifier).getEnabledFeatures()) - .noneMatch(t -> t == EE_FEATURES.ACCOUNT_LINKING)) { + .noneMatch(t -> t == EE_FEATURES.ACCOUNT_LINKING || t == EE_FEATURES.MFA)) { throw new FeatureNotEnabledException( "Account linking feature is not enabled for this app. Please contact support to enable it."); } AuthRecipeSQLStorage authRecipeStorage = StorageUtils.getAuthRecipeStorage(storage); try { - LinkAccountsResult result = authRecipeStorage.startTransaction(con -> { + LinkAccountsResult result = authRecipeStorage.startTransaction(con -> { try { CanLinkAccountsResult canLinkAccounts = canLinkAccountsHelper(con, appIdentifier, authRecipeStorage, _recipeUserId, _primaryUserId); @@ -537,7 +533,7 @@ public static CreatePrimaryUserResult createPrimaryUser(Main main, FeatureNotEnabledException { if (Arrays.stream(FeatureFlag.getInstance(main, appIdentifier).getEnabledFeatures()) - .noneMatch(t -> t == EE_FEATURES.ACCOUNT_LINKING)) { + .noneMatch(t -> t == EE_FEATURES.ACCOUNT_LINKING || t == EE_FEATURES.MFA)) { throw new FeatureNotEnabledException( "Account linking feature is not enabled for this app. Please contact support to enable it."); } @@ -911,7 +907,7 @@ public static void deleteUser(AppIdentifier appIdentifier, Storage storage, Stri } private static void deleteNonAuthRecipeUser(TransactionConnection con, AppIdentifier appIdentifier, - Storage storage, String userId) + Storage storage, String userId) throws StorageQueryException { StorageUtils.getUserMetadataStorage(storage) .deleteUserMetadata_Transaction(con, appIdentifier, userId); @@ -921,6 +917,7 @@ private static void deleteNonAuthRecipeUser(TransactionConnection con, AppIdenti .deleteEmailVerificationUserInfo_Transaction(con, appIdentifier, userId); StorageUtils.getUserRolesStorage(storage) .deleteAllRolesForUser_Transaction(con, appIdentifier, userId); + StorageUtils.getActiveUsersStorage(storage) .deleteUserActive_Transaction(con, appIdentifier, userId); StorageUtils.getTOTPStorage(storage) @@ -977,4 +974,4 @@ public UnlinkResult(String userId, boolean wasLinked) { this.wasLinked = wasLinked; } } -} +} \ No newline at end of file diff --git a/src/main/java/io/supertokens/emailpassword/EmailPassword.java b/src/main/java/io/supertokens/emailpassword/EmailPassword.java index 517cb3e3a..9a92592f3 100644 --- a/src/main/java/io/supertokens/emailpassword/EmailPassword.java +++ b/src/main/java/io/supertokens/emailpassword/EmailPassword.java @@ -38,6 +38,7 @@ 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.emailverification.sqlStorage.EmailVerificationSQLStorage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; @@ -110,14 +111,37 @@ public static AuthRecipeUserInfo signUp(TenantIdentifier tenantIdentifier, Stora .createHashWithSalt(tenantIdentifier.toAppIdentifier(), password); while (true) { - String userId = Utils.getUUID(); long timeJoined = System.currentTimeMillis(); try { - return StorageUtils.getEmailPasswordStorage(storage) + AuthRecipeUserInfo newUser = StorageUtils.getEmailPasswordStorage(storage) .signUp(tenantIdentifier, userId, email, hashedPassword, timeJoined); + if (Utils.isFakeEmail(email)) { + try { + EmailVerificationSQLStorage evStorage = StorageUtils.getEmailVerificationStorage(storage); + evStorage.startTransaction(con -> { + try { + evStorage.updateIsEmailVerified_Transaction(tenantIdentifier.toAppIdentifier(), con, + newUser.getSupertokensUserId(), email, true); + evStorage.commitTransaction(con); + + return null; + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } + }); + newUser.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 newUser; } catch (DuplicateUserIdException ignored) { // we retry with a new userId (while loop) } diff --git a/src/main/java/io/supertokens/featureflag/EE_FEATURES.java b/src/main/java/io/supertokens/featureflag/EE_FEATURES.java index 0f75c6014..e120fbf2f 100644 --- a/src/main/java/io/supertokens/featureflag/EE_FEATURES.java +++ b/src/main/java/io/supertokens/featureflag/EE_FEATURES.java @@ -18,8 +18,7 @@ public enum EE_FEATURES { ACCOUNT_LINKING("account_linking"), MULTI_TENANCY("multi_tenancy"), TEST("test"), - DASHBOARD_LOGIN("dashboard_login"), - TOTP("totp"); + DASHBOARD_LOGIN("dashboard_login"), MFA("mfa"); private final String name; diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 5d518faa9..b23434718 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -69,8 +69,8 @@ import io.supertokens.pluginInterface.totp.TOTPStorage; import io.supertokens.pluginInterface.totp.TOTPUsedCode; import io.supertokens.pluginInterface.totp.exception.DeviceAlreadyExistsException; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; +import io.supertokens.pluginInterface.totp.exception.UnknownTotpUserIdException; import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; import io.supertokens.pluginInterface.useridmapping.UserIdMapping; @@ -102,7 +102,7 @@ public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, TOTPSQLStorage, ActiveUsersStorage, - DashboardSQLStorage, AuthRecipeSQLStorage { + ActiveUsersSQLStorage, DashboardSQLStorage, AuthRecipeSQLStorage { private static final Object appenderLock = new Object(); private static final String APP_ID_KEY_NAME = "app_id"; @@ -520,11 +520,11 @@ public SessionInfo getSessionInfo_Transaction(TenantIdentifier tenantIdentifier, @Override public void updateSessionInfo_Transaction(TenantIdentifier tenantIdentifier, TransactionConnection con, String sessionHandle, String refreshTokenHash2, - long expiry) throws StorageQueryException { + long expiry, boolean useStaticKey) throws StorageQueryException { Connection sqlCon = (Connection) con.getConnection(); try { SessionQueries.updateSessionInfo_Transaction(this, sqlCon, tenantIdentifier, sessionHandle, - refreshTokenHash2, expiry); + refreshTokenHash2, expiry, useStaticKey); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -688,13 +688,14 @@ public void addInfoToNonAuthRecipesBasedOnUserId(TenantIdentifier tenantIdentifi } } else if (className.equals(TOTPStorage.class.getName())) { try { - TOTPDevice device = new TOTPDevice(userId, "testDevice", "secret", 0, 30, false); - TOTPQueries.createDevice(this, tenantIdentifier.toAppIdentifier(), device); + TOTPDevice device = new TOTPDevice(userId, "testDevice", "secret", 0, 30, false, System.currentTimeMillis()); this.startTransaction(con -> { try { long now = System.currentTimeMillis(); + Connection sqlCon = (Connection) con.getConnection(); + TOTPQueries.createDevice_Transaction(this, sqlCon, tenantIdentifier.toAppIdentifier(), device); TOTPQueries.insertUsedCode_Transaction(this, - (Connection) con.getConnection(), tenantIdentifier, + sqlCon, tenantIdentifier, new TOTPUsedCode(userId, "123456", true, 1000 + now, now)); } catch (SQLException e) { throw new StorageTransactionLogicException(e); @@ -1215,25 +1216,6 @@ public int countUsersActiveSince(AppIdentifier appIdentifier, long time) throws } } - @Override - public int countUsersEnabledTotp(AppIdentifier appIdentifier) throws StorageQueryException { - try { - return ActiveUsersQueries.countUsersEnabledTotp(this, appIdentifier); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - - @Override - public int countUsersEnabledTotpAndActiveSince(AppIdentifier appIdentifier, long time) - throws StorageQueryException { - try { - return ActiveUsersQueries.countUsersEnabledTotpAndActiveSince(this, appIdentifier, time); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - @Override public void deleteUserActive_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId) throws StorageQueryException { @@ -2196,10 +2178,11 @@ public boolean updateOrDeleteExternalUserIdInfo(AppIdentifier appIdentifier, Str } @Override - public HashMap getUserIdMappingForSuperTokensIds(ArrayList userIds) + public HashMap getUserIdMappingForSuperTokensIds(AppIdentifier appIdentifier, + ArrayList userIds) throws StorageQueryException { try { - return UserIdMappingQueries.getUserIdMappingWithUserIds(this, userIds); + return UserIdMappingQueries.getUserIdMappingWithUserIds(this, appIdentifier, userIds); } catch (SQLException e) { throw new StorageQueryException(e); } @@ -2589,26 +2572,56 @@ public void revokeExpiredSessions() throws StorageQueryException { // TOTP recipe: @Override public void createDevice(AppIdentifier appIdentifier, TOTPDevice device) - throws StorageQueryException, DeviceAlreadyExistsException, TenantOrAppNotFoundException { + throws DeviceAlreadyExistsException, TenantOrAppNotFoundException, StorageQueryException { try { - TOTPQueries.createDevice(this, appIdentifier, device); + startTransaction(con -> { + try { + createDevice_Transaction(con, new AppIdentifier(null, null), device); + } catch (DeviceAlreadyExistsException | TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } + return null; + }); } catch (StorageTransactionLogicException e) { - if (e.actualException instanceof SQLiteException) { - String errMsg = e.actualException.getMessage(); + if (e.actualException instanceof DeviceAlreadyExistsException) { + throw (DeviceAlreadyExistsException) e.actualException; + } else if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } else if (e.actualException instanceof StorageQueryException) { + throw (StorageQueryException) e.actualException; + } + } + } - if (isPrimaryKeyError(errMsg, Config.getConfig(this).getTotpUserDevicesTable(), - new String[]{"app_id", "user_id", "device_name"})) { - throw new DeviceAlreadyExistsException(); - } else if (isForeignKeyConstraintError( - errMsg, - Config.getConfig(this).getAppsTable(), - new String[]{"app_id"}, - new Object[]{appIdentifier.getAppId()})) { - throw new TenantOrAppNotFoundException(appIdentifier); - } + @Override + public TOTPDevice createDevice_Transaction(TransactionConnection con, AppIdentifier appIdentifier, TOTPDevice device) + throws DeviceAlreadyExistsException, TenantOrAppNotFoundException, StorageQueryException { + Connection sqlCon = (Connection) con.getConnection(); + try { + TOTPQueries.createDevice_Transaction(this, sqlCon, appIdentifier, device); + return device; + } catch (SQLException e) { + if (isPrimaryKeyError(e.getMessage(), Config.getConfig(this).getTotpUserDevicesTable(), + new String[]{"app_id", "user_id", "device_name"})) { + throw new DeviceAlreadyExistsException(); + } else if (isForeignKeyConstraintError( + e.getMessage(), + Config.getConfig(this).getAppsTable(), + new String[]{"app_id"}, + new Object[]{appIdentifier.getAppId()})) { + throw new TenantOrAppNotFoundException(appIdentifier); } + throw new StorageQueryException(e); + } + } - throw new StorageQueryException(e.actualException); + @Override + public TOTPDevice getDeviceByName_Transaction(TransactionConnection con, AppIdentifier appIdentifier, String userId, String deviceName) throws StorageQueryException { + Connection sqlCon = (Connection) con.getConnection(); + try { + return TOTPQueries.getDeviceByName_Transaction(this, sqlCon, appIdentifier, userId, deviceName); + } catch (SQLException e) { + throw new StorageQueryException(e); } } @@ -2705,7 +2718,7 @@ public TOTPDevice[] getDevices_Transaction(TransactionConnection con, AppIdentif @Override public void insertUsedCode_Transaction(TransactionConnection con, TenantIdentifier tenantIdentifier, TOTPUsedCode usedCodeObj) - throws StorageQueryException, TotpNotEnabledException, UsedCodeAlreadyExistsException, + throws StorageQueryException, UnknownTotpUserIdException, UsedCodeAlreadyExistsException, TenantOrAppNotFoundException { Connection sqlCon = (Connection) con.getConnection(); try { @@ -2720,7 +2733,7 @@ public void insertUsedCode_Transaction(TransactionConnection con, TenantIdentifi Config.getConfig(this).getTotpUsersTable(), new String[]{"app_id", "user_id"}, new Object[]{tenantIdentifier.getAppId(), usedCodeObj.userId})) { - throw new TotpNotEnabledException(); + throw new UnknownTotpUserIdException(); } else if (isForeignKeyConstraintError( e.getMessage(), @@ -2881,7 +2894,7 @@ public void linkAccounts_Transaction(AppIdentifier appIdentifier, TransactionCon GeneralQueries.linkAccounts_Transaction(this, sqlCon, appIdentifier, recipeUserId, primaryUserId); } catch (SQLException e) { throw new StorageQueryException(e); - } + } } @Override @@ -2955,4 +2968,21 @@ public UserIdMapping[] getUserIdMapping_Transaction(TransactionConnection con, A } } + @Override + public int getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(AppIdentifier appIdentifier) throws StorageQueryException { + try { + return GeneralQueries.getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(this, appIdentifier); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(AppIdentifier appIdentifier, long sinceTime) throws StorageQueryException { + try { + return ActiveUsersQueries.countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(this, appIdentifier, sinceTime); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } } diff --git a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java index 25fd59c61..ee8c43241 100644 --- a/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java +++ b/src/main/java/io/supertokens/inmemorydb/config/SQLiteConfig.java @@ -45,6 +45,14 @@ public String getTenantConfigsTable() { return "tenant_configs"; } + public String getTenantFirstFactorsTable() { + return "tenant_first_factors"; + } + + public String getTenantRequiredSecondaryFactorsTable() { + return "tenant_required_secondary_factors"; + } + public String getTenantThirdPartyProvidersTable() { return "tenant_thirdparty_providers"; } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java index 48aacda21..37635e1b8 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java @@ -41,6 +41,7 @@ public static int countUsersActiveSince(Start start, AppIdentifier appIdentifier public static int countUsersActiveSinceAndHasMoreThanOneLoginMethod(Start start, AppIdentifier appIdentifier, long sinceTime) throws SQLException, StorageQueryException { + // TODO: Active users are present only on public tenant and MFA users may be present on different storages 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() @@ -61,40 +62,6 @@ public static int countUsersActiveSinceAndHasMoreThanOneLoginMethod(Start start, }); } - 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 = ?"; - - return execute(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - }, result -> { - if (result.next()) { - return result.getInt("total"); - } - return 0; - }); - } - - 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 >= ?"; - - return execute(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setLong(2, sinceTime); - }, result -> { - if (result.next()) { - return result.getInt("total"); - } - return 0; - }); - } - public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, String userId) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getUserLastActiveTable() @@ -111,26 +78,6 @@ public static int updateUserLastActive(Start start, AppIdentifier appIdentifier, }); } - public static Long getLastActiveByUserId(Start start, AppIdentifier appIdentifier, String userId) - throws StorageQueryException { - String QUERY = "SELECT last_active_time FROM " + Config.getConfig(start).getUserLastActiveTable() - + " WHERE app_id = ? AND user_id = ?"; - - try { - return execute(start, QUERY, pst -> { - pst.setString(1, appIdentifier.getAppId()); - pst.setString(2, userId); - }, res -> { - if (res.next()) { - return res.getLong("last_active_time"); - } - return null; - }); - } catch (SQLException e) { - throw new StorageQueryException(e); - } - } - public static void deleteUserActive_Transaction(Connection con, Start start, AppIdentifier appIdentifier, String userId) throws StorageQueryException, SQLException { @@ -142,4 +89,41 @@ public static void deleteUserActive_Transaction(Connection con, Start start, App pst.setString(2, userId); }); } + + public static int countUsersThatHaveMoreThanOneLoginMethodOrTOTPEnabledAndActiveSince(Start start, AppIdentifier appIdentifier, long sinceTime) + throws SQLException, StorageQueryException { + // TODO: Active users are present only on public tenant and MFA users may be present on different storages + String QUERY = + "SELECT COUNT (DISTINCT user_id) as c FROM (" + + " " // users with more than one login method + + " SELECT primary_or_recipe_user_id AS user_id FROM (" + + " SELECT COUNT(user_id) as num_login_methods, app_id, primary_or_recipe_user_id" + + " FROM " + Config.getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? AND 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" + + " ) AS nloginmethods" + + " WHERE num_login_methods > 1" + + " UNION" // TOTP users + + " SELECT user_id FROM " + Config.getConfig(start).getTotpUsersTable() + + " WHERE app_id = ? AND user_id IN (" + + " SELECT user_id FROM " + Config.getConfig(start).getUserLastActiveTable() + + " WHERE app_id = ? AND last_active_time >= ?" + + " )" + + " " + + ") AS all_users"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, appIdentifier.getAppId()); + pst.setLong(3, sinceTime); + pst.setString(4, appIdentifier.getAppId()); + pst.setString(5, appIdentifier.getAppId()); + pst.setLong(6, sinceTime); + }, result -> { + return result.next() ? result.getInt("c") : 0; + }); + } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java index 934391294..78684eeac 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/EmailVerificationQueries.java @@ -289,7 +289,7 @@ public static List isEmailVerified_transaction(Start start, Connection s // calculating the verified emails HashMap supertokensUserIdToExternalUserIdMap = UserIdMappingQueries.getUserIdMappingWithUserIds_Transaction(start, - sqlCon, supertokensUserIds); + sqlCon, appIdentifier, supertokensUserIds); HashMap externalUserIdToSupertokensUserIdMap = new HashMap<>(); List supertokensOrExternalUserIdsToQuery = new ArrayList<>(); @@ -357,7 +357,7 @@ public static List isEmailVerified(Start start, AppIdentifier appIdentif // We have external user id stored in the email verification table, so we need to fetch the mapped userids for // calculating the verified emails HashMap supertokensUserIdToExternalUserIdMap = UserIdMappingQueries.getUserIdMappingWithUserIds(start, - supertokensUserIds); + appIdentifier, supertokensUserIds); HashMap externalUserIdToSupertokensUserIdMap = new HashMap<>(); List supertokensOrExternalUserIdsToQuery = new ArrayList<>(); for (String userId : supertokensUserIds) { diff --git a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java index c645f2f7a..46fc162af 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java @@ -182,7 +182,7 @@ static String getQueryToCreateTenantIdIndexForKeyValueTable(Start start) { + 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 + " (" @@ -259,6 +259,16 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc update(start, MultitenancyQueries.getQueryToCreateTenantConfigsTable(start), NO_OP_SETTER); } + if (!doesTableExists(start, Config.getConfig(start).getTenantFirstFactorsTable())) { + getInstance(main).addState(CREATING_NEW_TABLE, null); + update(start, MultitenancyQueries.getQueryToCreateFirstFactorsTable(start), NO_OP_SETTER); + } + + if (!doesTableExists(start, Config.getConfig(start).getTenantRequiredSecondaryFactorsTable())) { + getInstance(main).addState(CREATING_NEW_TABLE, null); + update(start, MultitenancyQueries.getQueryToCreateRequiredSecondaryFactorsTable(start), NO_OP_SETTER); + } + if (!doesTableExists(start, Config.getConfig(start).getTenantThirdPartyProvidersTable())) { getInstance(main).addState(CREATING_NEW_TABLE, null); update(start, MultitenancyQueries.getQueryToCreateTenantThirdPartyProvidersTable(start), @@ -1511,6 +1521,32 @@ public static int getUsersCountWithMoreThanOneLoginMethod(Start start, AppIdenti }); } + public static int getUsersCountWithMoreThanOneLoginMethodOrTOTPEnabled(Start start, AppIdentifier appIdentifier) + throws SQLException, StorageQueryException { + String QUERY = + "SELECT COUNT (DISTINCT user_id) as c FROM (" + + " " // Users with number of login methods > 1 + + " SELECT primary_or_recipe_user_id AS user_id FROM (" + + " SELECT COUNT(user_id) as num_login_methods, app_id, primary_or_recipe_user_id" + + " FROM " + getConfig(start).getAppIdToUserIdTable() + + " WHERE app_id = ? " + + " GROUP BY app_id, primary_or_recipe_user_id" + + " ) AS nloginmethods" + + " WHERE num_login_methods > 1" + + " UNION" // TOTP users + + " SELECT user_id FROM " + getConfig(start).getTotpUsersTable() + + " WHERE app_id = ?" + + " " + + ") AS all_users"; + + return execute(start, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, 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 " diff --git a/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java index 82af7379d..28da4e028 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/MultitenancyQueries.java @@ -18,6 +18,7 @@ import io.supertokens.inmemorydb.Start; import io.supertokens.inmemorydb.config.Config; +import io.supertokens.inmemorydb.queries.multitenancy.MfaSqlHelper; import io.supertokens.inmemorydb.queries.multitenancy.TenantConfigSQLHelper; import io.supertokens.inmemorydb.queries.multitenancy.ThirdPartyProviderClientSQLHelper; import io.supertokens.inmemorydb.queries.multitenancy.ThirdPartyProviderSQLHelper; @@ -51,6 +52,38 @@ static String getQueryToCreateTenantConfigsTable(Start start) { // @formatter:on } + public static String getQueryToCreateFirstFactorsTable(Start start) { + String tableName = Config.getConfig(start).getTenantFirstFactorsTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "connection_uri_domain VARCHAR(256) DEFAULT ''," + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "factor_id VARCHAR(128)," + + "PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id)," + + "FOREIGN KEY (connection_uri_domain, app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + + public static String getQueryToCreateRequiredSecondaryFactorsTable(Start start) { + String tableName = Config.getConfig(start).getTenantRequiredSecondaryFactorsTable(); + // @formatter:off + return "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + "connection_uri_domain VARCHAR(256) DEFAULT ''," + + "app_id VARCHAR(64) DEFAULT 'public'," + + "tenant_id VARCHAR(64) DEFAULT 'public'," + + "factor_id VARCHAR(128)," + + "PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id)," + + "FOREIGN KEY (connection_uri_domain, app_id, tenant_id)" + + " REFERENCES " + Config.getConfig(start).getTenantConfigsTable() + + " (connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE" + + ");"; + // @formatter:on + } + static String getQueryToCreateTenantThirdPartyProvidersTable(Start start) { String tenantThirdPartyProvidersTable = Config.getConfig(start).getTenantThirdPartyProvidersTable(); // @formatter:off @@ -114,6 +147,9 @@ private static void executeCreateTenantQueries(Start start, Connection sqlCon, T ThirdPartyProviderClientSQLHelper.create(start, sqlCon, tenantConfig, provider, providerClient); } } + + MfaSqlHelper.createFirstFactors(start, sqlCon, tenantConfig.tenantIdentifier, tenantConfig.firstFactors); + MfaSqlHelper.createRequiredSecondaryFactors(start, sqlCon, tenantConfig.tenantIdentifier, tenantConfig.requiredSecondaryFactors); } public static void createTenantConfig(Start start, TenantConfig tenantConfig) throws StorageQueryException, StorageTransactionLogicException { @@ -192,7 +228,13 @@ public static TenantConfig[] getAllTenants(Start start) throws StorageQueryExcep // Map (tenantIdentifier) -> thirdPartyId -> provider HashMap> providerMap = ThirdPartyProviderSQLHelper.selectAll(start, providerClientsMap); - return TenantConfigSQLHelper.selectAll(start, providerMap); + // Map (tenantIdentifier) -> firstFactors + HashMap firstFactorsMap = MfaSqlHelper.selectAllFirstFactors(start); + + // Map (tenantIdentifier) -> requiredSecondaryFactors + HashMap requiredSecondaryFactorsMap = MfaSqlHelper.selectAllRequiredSecondaryFactors(start); + + return TenantConfigSQLHelper.selectAll(start, providerMap, firstFactorsMap, requiredSecondaryFactorsMap); } catch (SQLException throwables) { throw new StorageQueryException(throwables); } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java index 65fa18c1a..d8e9a2b0d 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/SessionQueries.java @@ -147,18 +147,19 @@ public static SessionInfo getSessionInfo_Transaction(Start start, Connection con public static void updateSessionInfo_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier, String sessionHandle, - String refreshTokenHash2, long expiry) + String refreshTokenHash2, long expiry, boolean useStaticKey) throws SQLException, StorageQueryException { String QUERY = "UPDATE " + getConfig(start).getSessionInfoTable() - + " SET refresh_token_hash_2 = ?, expires_at = ?" + + " SET refresh_token_hash_2 = ?, expires_at = ?, use_static_key= ?" + " WHERE app_id = ? AND tenant_id = ? AND session_handle = ?"; update(con, QUERY, pst -> { pst.setString(1, refreshTokenHash2); pst.setLong(2, expiry); - pst.setString(3, tenantIdentifier.getAppId()); - pst.setString(4, tenantIdentifier.getTenantId()); - pst.setString(5, sessionHandle); + pst.setBoolean(3, useStaticKey); + pst.setString(4, tenantIdentifier.getAppId()); + pst.setString(5, tenantIdentifier.getTenantId()); + pst.setString(6, sessionHandle); }); } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/TOTPQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/TOTPQueries.java index bf5b17714..de5afc661 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/TOTPQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/TOTPQueries.java @@ -40,6 +40,7 @@ public static String getQueryToCreateUserDevicesTable(Start start) { + "period INTEGER NOT NULL," + "skew INTEGER NOT NULL," + "verified BOOLEAN NOT NULL," + + "created_at BIGINT UNSIGNED NOT NULL," + "PRIMARY KEY (app_id, user_id, device_name)," + "FOREIGN KEY (app_id, user_id) REFERENCES " + Config.getConfig(start).getTotpUsersTable() + " (app_id, user_id) ON DELETE CASCADE" @@ -85,7 +86,7 @@ private static int insertUser_Transaction(Start start, Connection con, AppIdenti private static int insertDevice_Transaction(Start start, Connection con, AppIdentifier appIdentifier, TOTPDevice device) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getTotpUserDevicesTable() - + " (app_id, user_id, device_name, secret_key, period, skew, verified) VALUES (?, ?, ?, ?, ?, ?, ?)"; + + " (app_id, user_id, device_name, secret_key, period, skew, verified, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; return update(con, QUERY, pst -> { pst.setString(1, appIdentifier.getAppId()); @@ -95,25 +96,35 @@ private static int insertDevice_Transaction(Start start, Connection con, AppIden pst.setInt(5, device.period); pst.setInt(6, device.skew); pst.setBoolean(7, device.verified); + pst.setLong(8, device.createdAt); }); } - public static void createDevice(Start start, AppIdentifier appIdentifier, TOTPDevice device) - throws StorageQueryException, StorageTransactionLogicException { - start.startTransaction(con -> { - Connection sqlCon = (Connection) con.getConnection(); - - try { - insertUser_Transaction(start, sqlCon, appIdentifier, device.userId); - insertDevice_Transaction(start, sqlCon, appIdentifier, device); - sqlCon.commit(); - } catch (SQLException e) { - throw new StorageTransactionLogicException(e); - } + public static void createDevice_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, TOTPDevice device) + throws StorageQueryException, SQLException { + insertUser_Transaction(start, sqlCon, appIdentifier, device.userId); + insertDevice_Transaction(start, sqlCon, appIdentifier, device); + } + + public static TOTPDevice getDeviceByName_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId, String deviceName) + throws SQLException, StorageQueryException { + + ((ConnectionWithLocks) sqlCon).lock( + appIdentifier.getAppId() + "~" + userId + "~" + deviceName + Config.getConfig(start).getTotpUserDevicesTable()); + + String QUERY = "SELECT * FROM " + Config.getConfig(start).getTotpUserDevicesTable() + + " WHERE app_id = ? AND user_id = ? AND device_name = ?;"; + return execute(sqlCon, QUERY, pst -> { + pst.setString(1, appIdentifier.getAppId()); + pst.setString(2, userId); + pst.setString(3, deviceName); + }, result -> { + if (result.next()) { + return TOTPDeviceRowMapper.getInstance().map(result); + } return null; }); - return; } public static int markDeviceAsVerified(Start start, AppIdentifier appIdentifier, String userId, String deviceName) @@ -290,7 +301,8 @@ public TOTPDevice map(ResultSet result) throws SQLException { result.getString("secret_key"), result.getInt("period"), result.getInt("skew"), - result.getBoolean("verified")); + result.getBoolean("verified"), + result.getLong("created_at")); } } diff --git a/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java index 579acc0b4..212ab44be 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/UserIdMappingQueries.java @@ -136,7 +136,8 @@ public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExter } - public static HashMap getUserIdMappingWithUserIds(Start start, List userIds) + public static HashMap getUserIdMappingWithUserIds(Start start, + AppIdentifier appIdentifier, List userIds) throws SQLException, StorageQueryException { if (userIds.size() == 0) { @@ -145,7 +146,8 @@ public static HashMap getUserIdMappingWithUserIds(Start start, L // No need to filter based on tenantId because the id list is already filtered for a tenant StringBuilder QUERY = new StringBuilder( - "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE supertokens_user_id IN ("); + "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND " + + "supertokens_user_id IN ("); for (int i = 0; i < userIds.size(); i++) { QUERY.append("?"); if (i != userIds.size() - 1) { @@ -155,9 +157,10 @@ public static HashMap getUserIdMappingWithUserIds(Start start, L } QUERY.append(")"); return execute(start, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); for (int i = 0; i < userIds.size(); i++) { - // i+1 cause this starts with 1 and not 0 - pst.setString(i + 1, userIds.get(i)); + // i+2 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 2, userIds.get(i)); } }, result -> { HashMap userIdMappings = new HashMap<>(); @@ -169,7 +172,9 @@ public static HashMap getUserIdMappingWithUserIds(Start start, L }); } - public static HashMap getUserIdMappingWithUserIds_Transaction(Start start, Connection sqlCon, List userIds) + public static HashMap getUserIdMappingWithUserIds_Transaction(Start start, Connection sqlCon, + AppIdentifier appIdentifier, + List userIds) throws SQLException, StorageQueryException { if (userIds.size() == 0) { @@ -178,7 +183,8 @@ public static HashMap getUserIdMappingWithUserIds_Transaction(St // No need to filter based on tenantId because the id list is already filtered for a tenant StringBuilder QUERY = new StringBuilder( - "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE supertokens_user_id IN ("); + "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE app_id = ? AND " + + "supertokens_user_id IN ("); for (int i = 0; i < userIds.size(); i++) { QUERY.append("?"); if (i != userIds.size() - 1) { @@ -188,9 +194,10 @@ public static HashMap getUserIdMappingWithUserIds_Transaction(St } QUERY.append(")"); return execute(sqlCon, QUERY.toString(), pst -> { + pst.setString(1, appIdentifier.getAppId()); for (int i = 0; i < userIds.size(); i++) { - // i+1 cause this starts with 1 and not 0 - pst.setString(i + 1, userIds.get(i)); + // i+2 cause this starts with 1 and not 0, and 1 is appId + pst.setString(i + 2, userIds.get(i)); } }, result -> { HashMap userIdMappings = new HashMap<>(); diff --git a/src/main/java/io/supertokens/inmemorydb/queries/multitenancy/MfaSqlHelper.java b/src/main/java/io/supertokens/inmemorydb/queries/multitenancy/MfaSqlHelper.java new file mode 100644 index 000000000..c1a65585f --- /dev/null +++ b/src/main/java/io/supertokens/inmemorydb/queries/multitenancy/MfaSqlHelper.java @@ -0,0 +1,116 @@ +/* + * 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.queries.multitenancy; + +import io.supertokens.inmemorydb.Start; +import io.supertokens.inmemorydb.config.Config; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.*; + +import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute; +import static io.supertokens.inmemorydb.QueryExecutorTemplate.update; + +public class MfaSqlHelper { + public static HashMap selectAllFirstFactors(Start start) + throws SQLException, StorageQueryException { + String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, factor_id FROM " + + Config.getConfig(start).getTenantFirstFactorsTable() + ";"; + return execute(start, QUERY, pst -> {}, result -> { + HashMap> firstFactors = new HashMap<>(); + + while (result.next()) { + TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), result.getString("app_id"), result.getString("tenant_id")); + if (!firstFactors.containsKey(tenantIdentifier)) { + firstFactors.put(tenantIdentifier, new ArrayList<>()); + } + + firstFactors.get(tenantIdentifier).add(result.getString("factor_id")); + } + + HashMap finalResult = new HashMap<>(); + for (TenantIdentifier tenantIdentifier : firstFactors.keySet()) { + finalResult.put(tenantIdentifier, firstFactors.get(tenantIdentifier).toArray(new String[0])); + } + + return finalResult; + }); + } + + public static HashMap selectAllRequiredSecondaryFactors(Start start) + throws SQLException, StorageQueryException { + String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, factor_id FROM " + + Config.getConfig(start).getTenantRequiredSecondaryFactorsTable() + ";"; + return execute(start, QUERY, pst -> {}, result -> { + HashMap> defaultRequiredFactors = new HashMap<>(); + + while (result.next()) { + TenantIdentifier tenantIdentifier = new TenantIdentifier(result.getString("connection_uri_domain"), + result.getString("app_id"), result.getString("tenant_id")); + if (!defaultRequiredFactors.containsKey(tenantIdentifier)) { + defaultRequiredFactors.put(tenantIdentifier, new ArrayList<>()); + } + + defaultRequiredFactors.get(tenantIdentifier).add(result.getString("factor_id")); + } + + HashMap finalResult = new HashMap<>(); + for (TenantIdentifier tenantIdentifier : defaultRequiredFactors.keySet()) { + finalResult.put(tenantIdentifier, defaultRequiredFactors.get(tenantIdentifier).toArray(new String[0])); + } + + return finalResult; + }); + } + + public static void createFirstFactors(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String[] firstFactors) + throws SQLException, StorageQueryException { + if (firstFactors == null || firstFactors.length == 0) { + return; + } + + String QUERY = "INSERT INTO " + Config.getConfig(start).getTenantFirstFactorsTable() + "(connection_uri_domain, app_id, tenant_id, factor_id) VALUES (?, ?, ?, ?);"; + for (String factorId : new HashSet<>(Arrays.asList(firstFactors))) { + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getConnectionUriDomain()); + pst.setString(2, tenantIdentifier.getAppId()); + pst.setString(3, tenantIdentifier.getTenantId()); + pst.setString(4, factorId); + }); + } + } + + public static void createRequiredSecondaryFactors(Start start, Connection sqlCon, TenantIdentifier tenantIdentifier, String[] requiredSecondaryFactors) + throws SQLException, StorageQueryException { + if (requiredSecondaryFactors == null || requiredSecondaryFactors.length == 0) { + return; + } + + String QUERY = "INSERT INTO " + Config.getConfig(start).getTenantRequiredSecondaryFactorsTable() + "(connection_uri_domain, app_id, tenant_id, factor_id) VALUES (?, ?, ?, ?);"; + for (String factorId : requiredSecondaryFactors) { + update(sqlCon, QUERY, pst -> { + pst.setString(1, tenantIdentifier.getConnectionUriDomain()); + pst.setString(2, tenantIdentifier.getAppId()); + pst.setString(3, tenantIdentifier.getTenantId()); + pst.setString(4, factorId); + }); + } + } +} diff --git a/src/main/java/io/supertokens/inmemorydb/queries/multitenancy/TenantConfigSQLHelper.java b/src/main/java/io/supertokens/inmemorydb/queries/multitenancy/TenantConfigSQLHelper.java index 6b5ce3931..7d994ed5a 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/multitenancy/TenantConfigSQLHelper.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/multitenancy/TenantConfigSQLHelper.java @@ -36,13 +36,17 @@ public class TenantConfigSQLHelper { public static class TenantConfigRowMapper implements RowMapper { ThirdPartyConfig.Provider[] providers; + String[] firstFactors; + String[] requiredSecondaryFactors; - private TenantConfigRowMapper(ThirdPartyConfig.Provider[] providers) { + private TenantConfigRowMapper(ThirdPartyConfig.Provider[] providers, String[] firstFactors, String[] requiredSecondaryFactors) { this.providers = providers; + this.firstFactors = firstFactors; + this.requiredSecondaryFactors = requiredSecondaryFactors; } - public static TenantConfigRowMapper getInstance(ThirdPartyConfig.Provider[] providers) { - return new TenantConfigRowMapper(providers); + public static TenantConfigRowMapper getInstance(ThirdPartyConfig.Provider[] providers, String[] firstFactors, String[] requiredSecondaryFactors) { + return new TenantConfigRowMapper(providers, firstFactors, requiredSecondaryFactors); } @Override @@ -53,6 +57,8 @@ public TenantConfig map(ResultSet result) throws StorageQueryException { new EmailPasswordConfig(result.getBoolean("email_password_enabled")), new ThirdPartyConfig(result.getBoolean("third_party_enabled"), this.providers), new PasswordlessConfig(result.getBoolean("passwordless_enabled")), + firstFactors.length == 0 ? null : firstFactors, + requiredSecondaryFactors.length == 0 ? null : requiredSecondaryFactors, JsonUtils.stringToJsonObject(result.getString("core_config")) ); } catch (Exception e) { @@ -61,7 +67,7 @@ public TenantConfig map(ResultSet result) throws StorageQueryException { } } - public static TenantConfig[] selectAll(Start start, HashMap> providerMap) + public static TenantConfig[] selectAll(Start start, HashMap> providerMap, HashMap firstFactorsMap, HashMap requiredSecondaryFactorsMap) throws SQLException, StorageQueryException { String QUERY = "SELECT connection_uri_domain, app_id, tenant_id, core_config, email_password_enabled, passwordless_enabled, third_party_enabled FROM " + Config.getConfig(start).getTenantConfigsTable() + ";"; @@ -74,7 +80,11 @@ public static TenantConfig[] selectAll(Start start, HashMap { pst.setString(1, tenantConfig.tenantIdentifier.getConnectionUriDomain()); diff --git a/src/main/java/io/supertokens/mfa/Mfa.java b/src/main/java/io/supertokens/mfa/Mfa.java new file mode 100644 index 000000000..a93f7b7c2 --- /dev/null +++ b/src/main/java/io/supertokens/mfa/Mfa.java @@ -0,0 +1,24 @@ +package io.supertokens.mfa; + +import io.supertokens.Main; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; + +public class Mfa { + public static void checkForMFAFeature(AppIdentifier appIdentifier, Main main) + throws StorageQueryException, TenantOrAppNotFoundException, FeatureNotEnabledException { + EE_FEATURES[] features = FeatureFlag.getInstance(main, appIdentifier).getEnabledFeatures(); + for (EE_FEATURES f : features) { + if (f == EE_FEATURES.MFA) { + return; + } + } + throw new FeatureNotEnabledException( + "MFA feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " + + "feature."); + } +} diff --git a/src/main/java/io/supertokens/multitenancy/Multitenancy.java b/src/main/java/io/supertokens/multitenancy/Multitenancy.java index 15666cc21..2342599dc 100644 --- a/src/main/java/io/supertokens/multitenancy/Multitenancy.java +++ b/src/main/java/io/supertokens/multitenancy/Multitenancy.java @@ -149,6 +149,51 @@ private static void validateTenantConfig(Main main, TenantConfig targetTenantCon // Verify that the keys in the coreConfig is valid validateConfigJsonForInvalidKeys(main, targetTenantConfig.coreConfig); + // Validate firstFactors and requiredSecondaryFactors + { + Set disallowedFactors = new HashSet<>(); + Map factorIdToRecipeName = new HashMap<>(); + if (!targetTenantConfig.emailPasswordConfig.enabled) { + disallowedFactors.add("emailpassword"); + + factorIdToRecipeName.put("emailpassword", "emailPassword"); + } + if (!targetTenantConfig.passwordlessConfig.enabled) { + disallowedFactors.add("otp-email"); + disallowedFactors.add("otp-phone"); + disallowedFactors.add("link-email"); + disallowedFactors.add("link-phone"); + + factorIdToRecipeName.put("otp-email", "passwordless"); + factorIdToRecipeName.put("otp-phone", "passwordless"); + factorIdToRecipeName.put("link-email", "passwordless"); + factorIdToRecipeName.put("link-phone", "passwordless"); + } + if (!targetTenantConfig.thirdPartyConfig.enabled) { + disallowedFactors.add("thirdparty"); + + factorIdToRecipeName.put("thirdparty", "thirdParty"); + } + + if (targetTenantConfig.firstFactors != null) { + for (String factor : targetTenantConfig.firstFactors) { + if (disallowedFactors.contains(factor)) { + throw new InvalidConfigException("firstFactors should not contain '" + factor + + "' because " + factorIdToRecipeName.get(factor) + " is disabled for the tenant."); + } + } + } + + if (targetTenantConfig.requiredSecondaryFactors != null) { + for (String factor : targetTenantConfig.requiredSecondaryFactors) { + if (disallowedFactors.contains(factor)) { + throw new InvalidConfigException("requiredSecondaryFactors should not contain '" + factor + + "' because " + factorIdToRecipeName.get(factor) + " is disabled for the tenant."); + } + } + } + } + // we check if the core config provided is correct { if (shouldPreventProtecterdConfigUpdate) { diff --git a/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java b/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java index ad0433238..63a5cd7b3 100644 --- a/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java +++ b/src/main/java/io/supertokens/multitenancy/MultitenancyHelper.java @@ -86,7 +86,8 @@ public static void init(Main main) throws StorageQueryException, IOException { new TenantConfig( new TenantIdentifier(null, null, null), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), - new PasswordlessConfig(true), new JsonObject()), false, false, false); + new PasswordlessConfig(true), + null, null, new JsonObject()), false, false, false); // Not force reloading all resources here (the last boolean in the function above) // because the ucl for the FeatureFlag is not yet loaded and results in an empty // instance of eeFeatureFlag. This is applicable only when the core is starting on @@ -106,7 +107,7 @@ private TenantConfig[] getAllTenantsFromDb() throws StorageQueryException { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ) }; } diff --git a/src/main/java/io/supertokens/output/CustomLayout.java b/src/main/java/io/supertokens/output/CustomLayout.java index 227a15e0a..0e9e57ef1 100644 --- a/src/main/java/io/supertokens/output/CustomLayout.java +++ b/src/main/java/io/supertokens/output/CustomLayout.java @@ -28,10 +28,12 @@ class CustomLayout extends LayoutBase { private String processID; + private String coreVersion; - CustomLayout(String processID) { + CustomLayout(String processID, String coreVersion) { super(); this.processID = processID; + this.coreVersion = coreVersion; } @Override @@ -49,6 +51,9 @@ public String doLayout(ILoggingEvent event) { sbuf.append(this.processID); sbuf.append(" | "); + sbuf.append("v" + coreVersion); + sbuf.append(" | "); + sbuf.append("["); sbuf.append(event.getThreadName()); sbuf.append("] thread"); diff --git a/src/main/java/io/supertokens/output/LayoutWrappingEncoder.java b/src/main/java/io/supertokens/output/LayoutWrappingEncoder.java index 91be454d9..b59d04b7b 100644 --- a/src/main/java/io/supertokens/output/LayoutWrappingEncoder.java +++ b/src/main/java/io/supertokens/output/LayoutWrappingEncoder.java @@ -27,8 +27,8 @@ class LayoutWrappingEncoder extends EncoderBase { private Layout layout; - LayoutWrappingEncoder(String processID) { - layout = new CustomLayout(processID); + LayoutWrappingEncoder(String processID, String coreVersion) { + layout = new CustomLayout(processID, coreVersion); } @Override diff --git a/src/main/java/io/supertokens/output/Logging.java b/src/main/java/io/supertokens/output/Logging.java index 8e1c4cddc..d3c89f1fb 100644 --- a/src/main/java/io/supertokens/output/Logging.java +++ b/src/main/java/io/supertokens/output/Logging.java @@ -30,6 +30,7 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.utils.Utils; +import io.supertokens.version.Version; import io.supertokens.webserver.Webserver; import org.slf4j.LoggerFactory; @@ -234,7 +235,7 @@ public static void stopLogging(Main main) { private Logger createLoggerForFile(Main main, String file, String name) { LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); - LayoutWrappingEncoder ple = new LayoutWrappingEncoder(main.getProcessId()); + LayoutWrappingEncoder ple = new LayoutWrappingEncoder(main.getProcessId(), Version.getVersion(main).getCoreVersion()); ple.setContext(lc); ple.start(); FileAppender fileAppender = new FileAppender<>(); @@ -252,7 +253,7 @@ private Logger createLoggerForFile(Main main, String file, String name) { private Logger createLoggerForConsole(Main main, String name, LOG_LEVEL logLevel) { LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); - LayoutWrappingEncoder ple = new LayoutWrappingEncoder(main.getProcessId()); + LayoutWrappingEncoder ple = new LayoutWrappingEncoder(main.getProcessId(), Version.getVersion(main).getCoreVersion()); ple.setContext(lc); ple.start(); ConsoleAppender logConsoleAppender = new ConsoleAppender<>(); diff --git a/src/main/java/io/supertokens/passwordless/Passwordless.java b/src/main/java/io/supertokens/passwordless/Passwordless.java index 7d51f1c9a..ba76d7f45 100644 --- a/src/main/java/io/supertokens/passwordless/Passwordless.java +++ b/src/main/java/io/supertokens/passwordless/Passwordless.java @@ -287,9 +287,9 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, false); } - public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, Storage storage, Main main, - String deviceId, String deviceIdHashFromUser, - String userInputCode, String linkCode, boolean setEmailVerified) + public static PasswordlessDevice checkCodeAndReturnDevice(TenantIdentifier tenantIdentifier, Storage storage, Main main, + String deviceId, String deviceIdHashFromUser, + String userInputCode, String linkCode, boolean deleteCodeOnSuccess) throws RestartFlowException, ExpiredUserInputCodeException, IncorrectUserInputCodeException, DeviceIdHashMismatchException, StorageTransactionLogicException, StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException, @@ -339,9 +339,8 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, throw new DeviceIdHashMismatchException(); } - PasswordlessDevice consumedDevice; try { - consumedDevice = passwordlessStorage.startTransaction(con -> { + return passwordlessStorage.startTransaction(con -> { PasswordlessDevice device = passwordlessStorage.getDevice_Transaction(tenantIdentifier, con, deviceIdHash.encode()); if (device == null) { @@ -386,12 +385,14 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, throw new StorageTransactionLogicException(new RestartFlowException()); } - if (device.email != null) { - passwordlessStorage.deleteDevicesByEmail_Transaction(tenantIdentifier, con, - device.email); - } else if (device.phoneNumber != null) { - passwordlessStorage.deleteDevicesByPhoneNumber_Transaction(tenantIdentifier, con, - device.phoneNumber); + if (deleteCodeOnSuccess) { + if (device.email != null) { + passwordlessStorage.deleteDevicesByEmail_Transaction(tenantIdentifier, con, + device.email); + } else if (device.phoneNumber != null) { + passwordlessStorage.deleteDevicesByPhoneNumber_Transaction(tenantIdentifier, con, + device.phoneNumber); + } } passwordlessStorage.commitTransaction(con); @@ -409,6 +410,20 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, } throw e; } + } + + public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, Storage storage, 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 { + + PasswordlessSQLStorage passwordlessStorage = StorageUtils.getPasswordlessStorage(storage); + + PasswordlessDevice consumedDevice = checkCodeAndReturnDevice(tenantIdentifier, storage, main, deviceId, deviceIdHashFromUser, + userInputCode, linkCode, true); // Getting here means that we successfully consumed the code AuthRecipeUserInfo user = null; @@ -452,13 +467,12 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, if (setEmailVerified && consumedDevice.email != null) { try { AuthRecipeUserInfo finalUser = user; - EmailVerificationSQLStorage evStorage = StorageUtils.getEmailVerificationStorage(storage); - + EmailVerificationSQLStorage evStorage = + StorageUtils.getEmailVerificationStorage(storage); evStorage.startTransaction(con -> { try { - evStorage.updateIsEmailVerified_Transaction( - tenantIdentifier.toAppIdentifier(), con, finalUser.getSupertokensUserId(), - consumedDevice.email, true); + evStorage.updateIsEmailVerified_Transaction(tenantIdentifier.toAppIdentifier(), con, + finalUser.getSupertokensUserId(), consumedDevice.email, true); evStorage.commitTransaction(con); return null; @@ -475,7 +489,7 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, } } - return new ConsumeCodeResponse(true, user, consumedDevice.email, consumedDevice.phoneNumber); + return new ConsumeCodeResponse(true, user, consumedDevice.email, consumedDevice.phoneNumber, consumedDevice); } 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 @@ -489,8 +503,6 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, } } } 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 (setEmailVerified && consumedDevice.email != null) { // Set email verification try { @@ -517,6 +529,8 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, } } + // We do need the cleanup here, however, we do not need this cleanup in the `if` block above + // since it uses the email/phoneNumber of the device, which has already been cleaned up if (loginMethod.email != null && !loginMethod.email.equals(consumedDevice.email)) { removeCodesByEmail(tenantIdentifier, storage, loginMethod.email); } @@ -524,7 +538,7 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifier tenantIdentifier, removeCodesByPhoneNumber(tenantIdentifier, storage, loginMethod.phoneNumber); } } - return new ConsumeCodeResponse(false, user, consumedDevice.email, consumedDevice.phoneNumber); + return new ConsumeCodeResponse(false, user, consumedDevice.email, consumedDevice.phoneNumber, consumedDevice); } @TestOnly @@ -569,6 +583,18 @@ public static void removeCode(TenantIdentifier tenantIdentifier, Storage storage }); } + public static void removeDevice(TenantIdentifier tenantIdentifier, Storage storage, + String deviceIdHash) + throws StorageQueryException, StorageTransactionLogicException { + PasswordlessSQLStorage passwordlessStorage = StorageUtils.getPasswordlessStorage(storage); + + passwordlessStorage.startTransaction(con -> { + passwordlessStorage.deleteDevice_Transaction(tenantIdentifier, con, deviceIdHash); + passwordlessStorage.commitTransaction(con); + return null; + }); + } + @TestOnly public static void removeCodesByEmail(Main main, String email) throws StorageQueryException, StorageTransactionLogicException { @@ -864,15 +890,20 @@ public CreateCodeResponse(String deviceIdHash, String codeId, String deviceId, S public static class ConsumeCodeResponse { public boolean createdNewUser; + + @Nullable public AuthRecipeUserInfo user; public String email; public String phoneNumber; - public ConsumeCodeResponse(boolean createdNewUser, AuthRecipeUserInfo user, String email, String phoneNumber) { + public PasswordlessDevice consumedDevice; + + public ConsumeCodeResponse(boolean createdNewUser, @Nullable AuthRecipeUserInfo user, String email, String phoneNumber, PasswordlessDevice consumedDevice) { this.createdNewUser = createdNewUser; this.user = user; this.email = email; this.phoneNumber = phoneNumber; + this.consumedDevice = consumedDevice; } } diff --git a/src/main/java/io/supertokens/session/Session.java b/src/main/java/io/supertokens/session/Session.java index c1969bd46..bf307be1f 100644 --- a/src/main/java/io/supertokens/session/Session.java +++ b/src/main/java/io/supertokens/session/Session.java @@ -49,6 +49,8 @@ import io.supertokens.session.jwt.JWT; import io.supertokens.session.refreshToken.RefreshToken; import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.useridmapping.UserIdMapping; +import io.supertokens.useridmapping.UserIdType; import io.supertokens.utils.Utils; import org.jetbrains.annotations.TestOnly; @@ -138,6 +140,12 @@ public static SessionInformationHolder createNewSession(TenantIdentifier tenantI sessionHandle += "_" + tenantIdentifier.getTenantId(); } + io.supertokens.pluginInterface.useridmapping.UserIdMapping userIdMapping = UserIdMapping.getUserIdMapping( + tenantIdentifier.toAppIdentifier(), storage, recipeUserId, UserIdType.EXTERNAL); + if (userIdMapping != null) { + recipeUserId = userIdMapping.superTokensUserId; + } + String primaryUserId = recipeUserId; if (storage.getType().equals(STORAGE_TYPE.SQL)) { primaryUserId = StorageUtils.getAuthRecipeStorage(storage) @@ -147,6 +155,16 @@ public static SessionInformationHolder createNewSession(TenantIdentifier tenantI } } + HashMap userIdMappings = UserIdMapping.getUserIdMappingForSuperTokensUserIds( + tenantIdentifier.toAppIdentifier(), storage, + new ArrayList<>(Arrays.asList(primaryUserId, recipeUserId))); + if (userIdMappings.containsKey(primaryUserId)) { + primaryUserId = userIdMappings.get(primaryUserId); + } + if (userIdMappings.containsKey(recipeUserId)) { + recipeUserId = userIdMappings.get(recipeUserId); + } + String antiCsrfToken = enableAntiCsrf ? UUID.randomUUID().toString() : null; final TokenInfo refreshToken = RefreshToken.createNewRefreshToken(tenantIdentifier, main, sessionHandle, recipeUserId, null, @@ -379,7 +397,7 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M accessToken.sessionHandle, Utils.hashSHA256(accessToken.refreshTokenHash1), System.currentTimeMillis() + - config.getRefreshTokenValidity()); + config.getRefreshTokenValidity(), sessionInfo.useStaticKey); } sessionStorage.commitTransaction(con); @@ -456,7 +474,7 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M Utils.hashSHA256(accessToken.refreshTokenHash1), System.currentTimeMillis() + Config.getConfig(tenantIdentifier, main) .getRefreshTokenValidity(), - sessionInfo.lastUpdatedSign); + sessionInfo.lastUpdatedSign, sessionInfo.useStaticKey); if (!success) { continue; } @@ -511,7 +529,7 @@ public static SessionInformationHolder refreshSession(Main main, @Nonnull String UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError { try { return refreshSession(new AppIdentifier(null, null), main, refreshToken, antiCsrfToken, - enableAntiCsrf, accessTokenVersion); + enableAntiCsrf, accessTokenVersion, null); } catch (TenantOrAppNotFoundException e) { throw new IllegalStateException(e); } @@ -520,7 +538,7 @@ public static SessionInformationHolder refreshSession(Main main, @Nonnull String public static SessionInformationHolder refreshSession(AppIdentifier appIdentifier, Main main, @Nonnull String refreshToken, @Nullable String antiCsrfToken, boolean enableAntiCsrf, - AccessToken.VERSION accessTokenVersion) + AccessToken.VERSION accessTokenVersion, Boolean shouldUseStaticKey) throws StorageTransactionLogicException, UnauthorisedException, StorageQueryException, TokenTheftDetectedException, UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError, TenantOrAppNotFoundException { @@ -537,14 +555,14 @@ public static SessionInformationHolder refreshSession(AppIdentifier appIdentifie TenantIdentifier tenantIdentifier = refreshTokenInfo.tenantIdentifier; Storage storage = StorageLayer.getStorage(refreshTokenInfo.tenantIdentifier, main); return refreshSessionHelper( - tenantIdentifier, storage, main, refreshToken, refreshTokenInfo, enableAntiCsrf, accessTokenVersion); + tenantIdentifier, storage, main, refreshToken, refreshTokenInfo, enableAntiCsrf, accessTokenVersion, shouldUseStaticKey); } private static SessionInformationHolder refreshSessionHelper( TenantIdentifier tenantIdentifier, Storage storage, Main main, String refreshToken, RefreshToken.RefreshTokenInfo refreshTokenInfo, boolean enableAntiCsrf, - AccessToken.VERSION accessTokenVersion) + AccessToken.VERSION accessTokenVersion, Boolean shouldUseStaticKey) throws StorageTransactionLogicException, UnauthorisedException, StorageQueryException, TokenTheftDetectedException, UnsupportedJWTSigningAlgorithmException, AccessTokenPayloadError, TenantOrAppNotFoundException { @@ -568,8 +586,16 @@ private static SessionInformationHolder refreshSessionHelper( sessionStorage.commitTransaction(con); throw new UnauthorisedException("Session missing in db or has expired"); } + boolean useStaticKey = shouldUseStaticKey != null ? shouldUseStaticKey : sessionInfo.useStaticKey; if (sessionInfo.refreshTokenHash2.equals(Utils.hashSHA256(Utils.hashSHA256(refreshToken)))) { + if (useStaticKey != sessionInfo.useStaticKey) { + // We do not update anything except the static key status + sessionStorage.updateSessionInfo_Transaction(tenantIdentifier, con, sessionHandle, + sessionInfo.refreshTokenHash2, sessionInfo.expiry, + useStaticKey); + } + // at this point, the input refresh token is the parent one. sessionStorage.commitTransaction(con); @@ -583,7 +609,8 @@ private static SessionInformationHolder refreshSessionHelper( sessionInfo.recipeUserId, sessionInfo.userId, Utils.hashSHA256(newRefreshToken.token), Utils.hashSHA256(refreshToken), sessionInfo.userDataInJWT, antiCsrfToken, - null, accessTokenVersion, sessionInfo.useStaticKey); + null, accessTokenVersion, + useStaticKey); TokenInfo idRefreshToken = new TokenInfo(UUID.randomUUID().toString(), newRefreshToken.expiry, newRefreshToken.createdTime); @@ -603,13 +630,13 @@ private static SessionInformationHolder refreshSessionHelper( .equals(sessionInfo.refreshTokenHash2))) { sessionStorage.updateSessionInfo_Transaction(tenantIdentifier, con, sessionHandle, Utils.hashSHA256(Utils.hashSHA256(refreshToken)), - System.currentTimeMillis() + config.getRefreshTokenValidity()); + System.currentTimeMillis() + config.getRefreshTokenValidity(), useStaticKey); sessionStorage.commitTransaction(con); return refreshSessionHelper(tenantIdentifier, storage, main, refreshToken, refreshTokenInfo, enableAntiCsrf, - accessTokenVersion); + accessTokenVersion, shouldUseStaticKey); } sessionStorage.commitTransaction(con); @@ -658,7 +685,18 @@ private static SessionInformationHolder refreshSessionHelper( throw new UnauthorisedException("Session missing in db or has expired"); } + boolean useStaticKey = shouldUseStaticKey != null ? shouldUseStaticKey : sessionInfo.useStaticKey; + if (sessionInfo.refreshTokenHash2.equals(Utils.hashSHA256(Utils.hashSHA256(refreshToken)))) { + if (sessionInfo.useStaticKey != useStaticKey) { + // We do not update anything except the static key status + boolean success = sessionStorage.updateSessionInfo_Transaction(sessionHandle, + sessionInfo.refreshTokenHash2, sessionInfo.expiry, + sessionInfo.lastUpdatedSign, useStaticKey); + if (!success) { + continue; + } + } // at this point, the input refresh token is the parent one. String antiCsrfToken = enableAntiCsrf ? UUID.randomUUID().toString() : null; @@ -669,7 +707,8 @@ private static SessionInformationHolder refreshSessionHelper( sessionHandle, sessionInfo.recipeUserId, sessionInfo.userId, Utils.hashSHA256(newRefreshToken.token), Utils.hashSHA256(refreshToken), sessionInfo.userDataInJWT, antiCsrfToken, - null, accessTokenVersion, sessionInfo.useStaticKey); + null, accessTokenVersion, + useStaticKey); TokenInfo idRefreshToken = new TokenInfo(UUID.randomUUID().toString(), newRefreshToken.expiry, newRefreshToken.createdTime); @@ -691,13 +730,13 @@ private static SessionInformationHolder refreshSessionHelper( Utils.hashSHA256(Utils.hashSHA256(refreshToken)), System.currentTimeMillis() + Config.getConfig(tenantIdentifier, main).getRefreshTokenValidity(), - sessionInfo.lastUpdatedSign); + sessionInfo.lastUpdatedSign, useStaticKey); if (!success) { continue; } - return refreshSessionHelper(tenantIdentifier, storage, main, refreshToken, refreshTokenInfo, - enableAntiCsrf, - accessTokenVersion); + return refreshSessionHelper( + tenantIdentifier, storage, main, refreshToken, refreshTokenInfo, + enableAntiCsrf, accessTokenVersion, shouldUseStaticKey); } throw new TokenTheftDetectedException(sessionHandle, sessionInfo.recipeUserId, sessionInfo.userId); diff --git a/src/main/java/io/supertokens/totp/Totp.java b/src/main/java/io/supertokens/totp/Totp.java index 58d252410..e76dd6182 100644 --- a/src/main/java/io/supertokens/totp/Totp.java +++ b/src/main/java/io/supertokens/totp/Totp.java @@ -3,21 +3,20 @@ import com.eatthepath.otp.TimeBasedOneTimePasswordGenerator; import io.supertokens.Main; import io.supertokens.config.Config; -import io.supertokens.featureflag.EE_FEATURES; -import io.supertokens.featureflag.FeatureFlag; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; -import io.supertokens.pluginInterface.Storage; -import io.supertokens.pluginInterface.StorageUtils; +import io.supertokens.mfa.Mfa; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.StorageUtils; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.totp.TOTPDevice; import io.supertokens.pluginInterface.totp.TOTPUsedCode; import io.supertokens.pluginInterface.totp.exception.DeviceAlreadyExistsException; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; +import io.supertokens.pluginInterface.totp.exception.UnknownTotpUserIdException; import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; import io.supertokens.storageLayer.StorageLayer; @@ -71,21 +70,9 @@ private static boolean checkCode(TOTPDevice device, String code) { return false; } - private static boolean isTotpEnabled(AppIdentifier appIdentifier, Main main) - throws StorageQueryException, TenantOrAppNotFoundException { - EE_FEATURES[] features = FeatureFlag.getInstance(main, appIdentifier).getEnabledFeatures(); - for (EE_FEATURES f : features) { - if (f == EE_FEATURES.TOTP) { - return true; - } - } - return false; - } - - @TestOnly public static TOTPDevice registerDevice(Main main, String userId, - String deviceName, int skew, int period) + String deviceName, int skew, int period) throws StorageQueryException, DeviceAlreadyExistsException, NoSuchAlgorithmException, FeatureNotEnabledException { try { @@ -96,30 +83,81 @@ public static TOTPDevice registerDevice(Main main, String userId, } } - public static TOTPDevice registerDevice(AppIdentifier appIdentifier, Storage storage, Main main, String userId, - String deviceName, int skew, int period) - throws StorageQueryException, DeviceAlreadyExistsException, NoSuchAlgorithmException, - FeatureNotEnabledException, TenantOrAppNotFoundException { + public static TOTPDevice createDevice(Main main, AppIdentifier appIdentifier, Storage storage, String userId, + String deviceName, int skew, int period, String secretKey, boolean verified, + long createdAt) + throws DeviceAlreadyExistsException, StorageQueryException, FeatureNotEnabledException, + TenantOrAppNotFoundException { + + Mfa.checkForMFAFeature(appIdentifier, main); - if (!isTotpEnabled(appIdentifier, main)) { - throw new FeatureNotEnabledException( - "TOTP feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " + - "feature."); + if (deviceName != null) { + TOTPSQLStorage totpStorage = StorageUtils.getTOTPStorage(storage); + try { + return totpStorage.startTransaction(con -> { + try { + TOTPDevice existingDevice = totpStorage.getDeviceByName_Transaction(con, appIdentifier, userId, + deviceName); + if (existingDevice == null) { + return totpStorage.createDevice_Transaction(con, appIdentifier, new TOTPDevice( + userId, deviceName, secretKey, period, skew, verified, createdAt + )); + } else if (!existingDevice.verified) { + totpStorage.deleteDevice_Transaction(con, appIdentifier, userId, deviceName); + return totpStorage.createDevice_Transaction(con, appIdentifier, new TOTPDevice( + userId, deviceName, secretKey, period, skew, verified, createdAt + )); + } else { + throw new StorageTransactionLogicException(new DeviceAlreadyExistsException()); + } + } catch (TenantOrAppNotFoundException | DeviceAlreadyExistsException e) { + throw new StorageTransactionLogicException(e); + } + }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof DeviceAlreadyExistsException) { + throw (DeviceAlreadyExistsException) e.actualException; + } + throw new StorageQueryException(e.actualException); + } } TOTPSQLStorage totpStorage = StorageUtils.getTOTPStorage(storage); + TOTPDevice[] devices = totpStorage.getDevices(appIdentifier, userId); + int verifiedDevicesCount = Arrays.stream(devices).filter(d -> d.verified).toArray().length; - String secret = generateSecret(); - TOTPDevice device = new TOTPDevice(userId, deviceName, secret, period, skew, false); - totpStorage.createDevice(appIdentifier, device); + while (true) { + try { + return createDevice(main, appIdentifier, storage, + userId, + "TOTP Device " + verifiedDevicesCount, + skew, + period, + secretKey, + verified, + createdAt + ); + } catch (DeviceAlreadyExistsException e){ + } + verifiedDevicesCount++; + } + } - return device; + public static TOTPDevice registerDevice(AppIdentifier appIdentifier, Storage storage, Main main, String userId, + String deviceName, int skew, int period) + throws StorageQueryException, DeviceAlreadyExistsException, NoSuchAlgorithmException, + FeatureNotEnabledException, TenantOrAppNotFoundException { + + String secretKey = generateSecret(); + + return createDevice(main, appIdentifier, storage, userId, deviceName, skew, period, secretKey, false, + System.currentTimeMillis()); } private static void checkAndStoreCode(TenantIdentifier tenantIdentifier, Storage storage, Main main, - String userId, TOTPDevice[] devices, - String code) - throws InvalidTotpException, TotpNotEnabledException, + String userId, TOTPDevice[] devices, + String code) + throws InvalidTotpException, UnknownTotpUserIdException, LimitReachedException, StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException { // Note that the TOTP cron runs every 1 hour, so all the expired tokens can stay @@ -157,134 +195,125 @@ private static void checkAndStoreCode(TenantIdentifier tenantIdentifier, Storage TOTPSQLStorage totpSQLStorage = StorageUtils.getTOTPStorage(storage); - while (true) { - try { - totpSQLStorage.startTransaction(con -> { - try { - TOTPUsedCode[] usedCodes = totpSQLStorage.getAllUsedCodesDescOrder_Transaction(con, - tenantIdentifier, userId); - - // N represents # of invalid attempts that will trigger rate limiting: - int N = Config.getConfig(tenantIdentifier, main).getTotpMaxAttempts(); // (Default 5) - // Count # of contiguous invalids in latest N attempts (stop at first valid): - long invalidOutOfN = Arrays.stream(usedCodes).limit(N).takeWhile(usedCode -> !usedCode.isValid) - .count(); - int rateLimitResetTimeInMs = - Config.getConfig(tenantIdentifier, main).getTotpRateLimitCooldownTimeSec() * - 1000; // (Default - // 15 mins) - - // Check if the user has been rate limited: - if (invalidOutOfN == N) { - // All of the latest N attempts were invalid: - long latestInvalidCodeCreatedTime = usedCodes[0].createdTime; - long now = System.currentTimeMillis(); - - if (now - latestInvalidCodeCreatedTime < rateLimitResetTimeInMs) { - // Less than rateLimitResetTimeInMs (default = 15 mins) time has elasped since - // the last invalid code: - long timeLeftMs = (rateLimitResetTimeInMs - (now - latestInvalidCodeCreatedTime)); - throw new StorageTransactionLogicException(new LimitReachedException(timeLeftMs)); - - // If we insert the used code here, then it will further delay the user from - // being able to login. So not inserting it here. - } - } + try { + totpSQLStorage.startTransaction(con -> { + try { + TOTPUsedCode[] usedCodes = totpSQLStorage.getAllUsedCodesDescOrder_Transaction(con, + tenantIdentifier, userId); + + // N represents # of invalid attempts that will trigger rate limiting: + int N = Config.getConfig(tenantIdentifier, main).getTotpMaxAttempts(); // (Default 5) + // Count # of contiguous invalids in latest N attempts (stop at first valid): + long invalidOutOfN = Arrays.stream(usedCodes).limit(N).takeWhile(usedCode -> !usedCode.isValid) + .count(); + int rateLimitResetTimeInMs = Config.getConfig(tenantIdentifier, main) + .getTotpRateLimitCooldownTimeSec() * + 1000; // (Default 15 mins) + + // Check if the user has been rate limited: + if (invalidOutOfN == N) { + // All of the latest N attempts were invalid: + long latestInvalidCodeCreatedTime = usedCodes[0].createdTime; + long now = System.currentTimeMillis(); - // Check if the code is valid for any device: - boolean isValid = false; - TOTPDevice matchingDevice = null; - for (TOTPDevice device : devices) { - // Check if the code is valid for this device: - if (checkCode(device, code)) { - isValid = true; - matchingDevice = device; - break; - } - } + if (now - latestInvalidCodeCreatedTime < rateLimitResetTimeInMs) { + // Less than rateLimitResetTimeInMs (default = 15 mins) time has elasped since + // the last invalid code: + long timeLeftMs = (rateLimitResetTimeInMs - (now - latestInvalidCodeCreatedTime)); + throw new StorageTransactionLogicException(new LimitReachedException(timeLeftMs, (int)invalidOutOfN, N)); - // Check if the code has been previously used by the user and it was valid (and - // is still valid). If so, this could be a replay attack. So reject it. - if (isValid) { - for (TOTPUsedCode usedCode : usedCodes) { - // One edge case is that if the user has 2 devices, and they are used back to - // back (within 90 seconds) such that the code of the first device was - // regenerated by the second device, then it won't allow the second device's - // code to be used until it is expired. - // But this would be rare so we can ignore it for now. - if (usedCode.code.equals(code) && usedCode.isValid - && usedCode.expiryTime > System.currentTimeMillis()) { - isValid = false; - // We found a matching device but the code - // will be considered invalid here. - } - } + // If we insert the used code here, then it will further delay the user from + // being able to login. So not inserting it here. } + } - // Insert the code into the list of used codes: - - // If device is found, calculate used code expiry time for that device (based on - // its period and skew). Otherwise, use the max used code expiry time of all the - // devices. - int maxUsedCodeExpiry = Arrays.stream(devices) - .mapToInt(device -> device.period * (2 * device.skew + 1)) - .max() - .orElse(0); - int expireInSec = - (matchingDevice != null) ? matchingDevice.period * (2 * matchingDevice.skew + 1) - : maxUsedCodeExpiry; - - long now = System.currentTimeMillis(); - TOTPUsedCode newCode = new TOTPUsedCode(userId, - code, - isValid, now + 1000 * expireInSec, now); - try { - totpSQLStorage.insertUsedCode_Transaction(con, tenantIdentifier, newCode); - totpSQLStorage.commitTransaction(con); - } catch (UsedCodeAlreadyExistsException | TotpNotEnabledException e) { - throw new StorageTransactionLogicException(e); + // Check if the code is valid for any device: + boolean isValid = false; + TOTPDevice matchingDevice = null; + for (TOTPDevice device : devices) { + // Check if the code is valid for this device: + if (checkCode(device, code)) { + isValid = true; + matchingDevice = device; + break; } + } - if (!isValid) { - // transaction has been committed, so we can directly throw the exception: - throw new StorageTransactionLogicException(new InvalidTotpException()); + // Check if the code has been previously used by the user and it was valid (and + // is still valid). If so, this could be a replay attack. So reject it. + if (isValid) { + for (TOTPUsedCode usedCode : usedCodes) { + // One edge case is that if the user has 2 devices, and they are used back to + // back (within 90 seconds) such that the code of the first device was + // regenerated by the second device, then it won't allow the second device's + // code to be used until it is expired. + // But this would be rare so we can ignore it for now. + if (usedCode.code.equals(code) && usedCode.isValid + && usedCode.expiryTime > System.currentTimeMillis()) { + isValid = false; + // We found a matching device but the code + // will be considered invalid here. + } } + } - return null; - } catch (TenantOrAppNotFoundException e) { + // Insert the code into the list of used codes: + + // If device is found, calculate used code expiry time for that device (based on + // its period and skew). Otherwise, use the max used code expiry time of all the + // devices. + int maxUsedCodeExpiry = Arrays.stream(devices) + .mapToInt(device -> device.period * (2 * device.skew + 1)) + .max() + .orElse(0); + int expireInSec = (matchingDevice != null) + ? matchingDevice.period * (2 * matchingDevice.skew + 1) + : maxUsedCodeExpiry; + + long now = System.currentTimeMillis(); + TOTPUsedCode newCode = new TOTPUsedCode(userId, + code, + isValid, now + 1000L * expireInSec, now); + try { + totpSQLStorage.insertUsedCode_Transaction(con, tenantIdentifier, newCode); + totpSQLStorage.commitTransaction(con); + } catch (UnknownTotpUserIdException e) { throw new StorageTransactionLogicException(e); + } catch (UsedCodeAlreadyExistsException e) { + throw new StorageTransactionLogicException(new InvalidTotpException((int) invalidOutOfN, N)); } - }); - return; // exit the while loop - } catch (StorageTransactionLogicException e) { - // throwing errors will also help exit the while loop: - if (e.actualException instanceof TenantOrAppNotFoundException) { - throw (TenantOrAppNotFoundException) e.actualException; - } else if (e.actualException instanceof LimitReachedException) { - throw (LimitReachedException) e.actualException; - } else if (e.actualException instanceof InvalidTotpException) { - throw (InvalidTotpException) e.actualException; - } else if (e.actualException instanceof TotpNotEnabledException) { - throw (TotpNotEnabledException) e.actualException; - } else if (e.actualException instanceof UsedCodeAlreadyExistsException) { - // retry the transaction after a small delay: - int delayInMs = (int) (Math.random() * 10 + 1); - try { - Thread.sleep(delayInMs); - } catch (InterruptedException ignored) { - // ignore the error and retry + + if (!isValid) { + // transaction has been committed, so we can directly throw the exception: + throw new StorageTransactionLogicException(new InvalidTotpException((int)invalidOutOfN+1, N)); } - } else { - throw e; + + return null; + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); } + }); + return; // exit the while loop + } catch (StorageTransactionLogicException e) { + // throwing errors will also help exit the while loop: + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; + } else if (e.actualException instanceof LimitReachedException) { + throw (LimitReachedException) e.actualException; + } else if (e.actualException instanceof InvalidTotpException) { + throw (InvalidTotpException) e.actualException; + } else if (e.actualException instanceof UnknownTotpUserIdException) { + throw (UnknownTotpUserIdException) e.actualException; + } else { + throw e; } } } @TestOnly public static boolean verifyDevice(Main main, - String userId, String deviceName, String code) - throws TotpNotEnabledException, UnknownDeviceException, InvalidTotpException, + String userId, String deviceName, String code) + throws UnknownDeviceException, InvalidTotpException, LimitReachedException, StorageQueryException, StorageTransactionLogicException { try { return verifyDevice(new TenantIdentifier(null, null, null), @@ -295,8 +324,8 @@ public static boolean verifyDevice(Main main, } public static boolean verifyDevice(TenantIdentifier tenantIdentifier, Storage storage, Main main, - String userId, String deviceName, String code) - throws TotpNotEnabledException, UnknownDeviceException, InvalidTotpException, + String userId, String deviceName, String code) + throws UnknownDeviceException, InvalidTotpException, LimitReachedException, StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException { // Here boolean return value tells whether the device has been @@ -312,7 +341,7 @@ public static boolean verifyDevice(TenantIdentifier tenantIdentifier, Storage st // Check if the user has any devices: TOTPDevice[] devices = totpStorage.getDevices(tenantIdentifier.toAppIdentifier(), userId); if (devices.length == 0) { - throw new TotpNotEnabledException(); + throw new UnknownDeviceException(); } // Check if the requested device exists: @@ -337,62 +366,63 @@ public static boolean verifyDevice(TenantIdentifier tenantIdentifier, Storage st // verified in the devices table (because it was deleted/renamed). So the user // gets a UnknownDevceException. // This behaviour is okay so we can ignore it. - checkAndStoreCode(tenantIdentifier, storage, main, userId, new TOTPDevice[]{matchingDevice}, - code); + try { + checkAndStoreCode(tenantIdentifier, storage, main, userId, new TOTPDevice[] { matchingDevice }, code); + } catch (UnknownTotpUserIdException e) { + // User must have deleted the device in parallel. + throw new UnknownDeviceException(); + } // Will reach here only if the code is valid: totpStorage.markDeviceAsVerified(tenantIdentifier.toAppIdentifier(), userId, deviceName); return true; // Newly verified } @TestOnly - public static void verifyCode(Main main, String userId, - String code, boolean allowUnverifiedDevices) - throws TotpNotEnabledException, InvalidTotpException, LimitReachedException, + public static void verifyCode(Main main, String userId, String code) + throws InvalidTotpException, UnknownTotpUserIdException, LimitReachedException, StorageQueryException, StorageTransactionLogicException, FeatureNotEnabledException { try { - verifyCode(new TenantIdentifier(null, null, null), - StorageLayer.getStorage(main), main, userId, code, allowUnverifiedDevices); + verifyCode(new TenantIdentifier(null, null, null), StorageLayer.getStorage(main), main, + userId, code); } catch (TenantOrAppNotFoundException e) { throw new IllegalStateException(e); } } - public static void verifyCode(TenantIdentifier tenantIdentifier, Storage storage, Main main, String userId, - String code, boolean allowUnverifiedDevices) - throws TotpNotEnabledException, InvalidTotpException, LimitReachedException, + public static void verifyCode(TenantIdentifier tenantIdentifier, Storage storage, Main main, String userId, String code) + throws InvalidTotpException, UnknownTotpUserIdException, LimitReachedException, StorageQueryException, StorageTransactionLogicException, FeatureNotEnabledException, TenantOrAppNotFoundException { - if (!isTotpEnabled(tenantIdentifier.toAppIdentifier(), main)) { - throw new FeatureNotEnabledException( - "TOTP feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " + - "feature."); - } + Mfa.checkForMFAFeature(tenantIdentifier.toAppIdentifier(), main); TOTPSQLStorage totpStorage = StorageUtils.getTOTPStorage(storage); // Check if the user has any devices: TOTPDevice[] devices = totpStorage.getDevices(tenantIdentifier.toAppIdentifier(), userId); if (devices.length == 0) { - throw new TotpNotEnabledException(); + // No devices found. So we can't verify the code anyway. + throw new UnknownTotpUserIdException(); } // Filter out unverified devices: - if (!allowUnverifiedDevices) { - devices = Arrays.stream(devices).filter(device -> device.verified).toArray(TOTPDevice[]::new); - } + devices = Arrays.stream(devices).filter(device -> device.verified).toArray(TOTPDevice[]::new); // At this point, even if some of the devices are suddenly deleted/renamed by // another API call. We will still check the code against the updated set of // devices and store it in the used codes table. This behaviour is okay so we // can ignore it. + + // UnknownTotpUserIdException will be thrown when + // the User has deleted the device in parallel + // since they cannot un-verify a device (no API exists) checkAndStoreCode(tenantIdentifier, storage, main, userId, devices, code); } @TestOnly public static void removeDevice(Main main, String userId, - String deviceName) - throws StorageQueryException, UnknownDeviceException, TotpNotEnabledException, + String deviceName) + throws StorageQueryException, UnknownDeviceException, StorageTransactionLogicException { try { removeDevice(new AppIdentifier(null, null), StorageLayer.getStorage(main), @@ -406,8 +436,8 @@ public static void removeDevice(Main main, String userId, * Delete device and also delete the user if deleting the last device */ public static void removeDevice(AppIdentifier appIdentifier, Storage storage, String userId, - String deviceName) - throws StorageQueryException, UnknownDeviceException, TotpNotEnabledException, + String deviceName) + throws StorageQueryException, UnknownDeviceException, StorageTransactionLogicException, TenantOrAppNotFoundException { TOTPSQLStorage totpStorage = StorageUtils.getTOTPStorage(storage); @@ -432,12 +462,6 @@ public static void removeDevice(AppIdentifier appIdentifier, Storage storage, St return; } catch (StorageTransactionLogicException e) { if (e.actualException instanceof UnknownDeviceException) { - // Check if any device exists for the user: - TOTPDevice[] devices = totpStorage.getDevices(appIdentifier, userId); - if (devices.length == 0) { - throw new TotpNotEnabledException(); - } - throw (UnknownDeviceException) e.actualException; } @@ -447,9 +471,8 @@ public static void removeDevice(AppIdentifier appIdentifier, Storage storage, St @TestOnly public static void updateDeviceName(Main main, String userId, - String oldDeviceName, String newDeviceName) - throws StorageQueryException, DeviceAlreadyExistsException, UnknownDeviceException, - TotpNotEnabledException { + String oldDeviceName, String newDeviceName) + throws StorageQueryException, DeviceAlreadyExistsException, UnknownDeviceException { try { updateDeviceName(new AppIdentifier(null, null), StorageLayer.getStorage(main), userId, oldDeviceName, newDeviceName); @@ -459,26 +482,16 @@ public static void updateDeviceName(Main main, String userId, } public static void updateDeviceName(AppIdentifier appIdentifier, Storage storage, String userId, - String oldDeviceName, String newDeviceName) + String oldDeviceName, String newDeviceName) throws StorageQueryException, DeviceAlreadyExistsException, UnknownDeviceException, - TotpNotEnabledException, TenantOrAppNotFoundException { + TenantOrAppNotFoundException { TOTPSQLStorage totpStorage = StorageUtils.getTOTPStorage(storage); - try { - totpStorage.updateDeviceName(appIdentifier, userId, oldDeviceName, newDeviceName); - } catch (UnknownDeviceException e) { - // Check if any device exists for the user: - TOTPDevice[] devices = totpStorage.getDevices(appIdentifier, userId); - if (devices.length == 0) { - throw new TotpNotEnabledException(); - } else { - throw e; - } - } + totpStorage.updateDeviceName(appIdentifier, userId, oldDeviceName, newDeviceName); } @TestOnly public static TOTPDevice[] getDevices(Main main, String userId) - throws StorageQueryException, TotpNotEnabledException { + throws StorageQueryException { try { return getDevices(new AppIdentifier(null, null), StorageLayer.getStorage(main), userId); @@ -488,14 +501,10 @@ public static TOTPDevice[] getDevices(Main main, String userId) } public static TOTPDevice[] getDevices(AppIdentifier appIdentifier, Storage storage, String userId) - throws StorageQueryException, TotpNotEnabledException, TenantOrAppNotFoundException { + throws StorageQueryException, TenantOrAppNotFoundException { TOTPSQLStorage totpStorage = StorageUtils.getTOTPStorage(storage); TOTPDevice[] devices = totpStorage.getDevices(appIdentifier, userId); - if (devices.length == 0) { - throw new TotpNotEnabledException(); - } return devices; } - } diff --git a/src/main/java/io/supertokens/totp/exceptions/InvalidTotpException.java b/src/main/java/io/supertokens/totp/exceptions/InvalidTotpException.java index 9dce2f51d..fc6dd25f2 100644 --- a/src/main/java/io/supertokens/totp/exceptions/InvalidTotpException.java +++ b/src/main/java/io/supertokens/totp/exceptions/InvalidTotpException.java @@ -1,5 +1,12 @@ package io.supertokens.totp.exceptions; public class InvalidTotpException extends Exception { + public int currentAttempts; + public int maxAttempts; + public InvalidTotpException(int currentAttempts, int maxAttempts) { + super("Invalid totp"); + this.currentAttempts = currentAttempts; + this.maxAttempts = maxAttempts; + } } diff --git a/src/main/java/io/supertokens/totp/exceptions/LimitReachedException.java b/src/main/java/io/supertokens/totp/exceptions/LimitReachedException.java index b7b1c8078..635aad73d 100644 --- a/src/main/java/io/supertokens/totp/exceptions/LimitReachedException.java +++ b/src/main/java/io/supertokens/totp/exceptions/LimitReachedException.java @@ -3,9 +3,13 @@ public class LimitReachedException extends Exception { public long retryAfterMs; + public int currentAttempts; + public int maxAttempts; - public LimitReachedException(long retryAfterMs) { + public LimitReachedException(long retryAfterMs, int currentAttempts, int maxAttempts) { super("Retry in " + retryAfterMs + " ms"); this.retryAfterMs = retryAfterMs; + this.currentAttempts = currentAttempts; + this.maxAttempts = maxAttempts; } } diff --git a/src/main/java/io/supertokens/useridmapping/UserIdMapping.java b/src/main/java/io/supertokens/useridmapping/UserIdMapping.java index 274e08efc..5f6482b20 100644 --- a/src/main/java/io/supertokens/useridmapping/UserIdMapping.java +++ b/src/main/java/io/supertokens/useridmapping/UserIdMapping.java @@ -338,11 +338,12 @@ public static boolean updateOrDeleteExternalUserIdInfo(Main main, } public static HashMap getUserIdMappingForSuperTokensUserIds( + AppIdentifier appIdentifier, Storage storage, ArrayList userIds) throws StorageQueryException { // userIds are already filtered for a tenant - return StorageUtils.getUserIdMappingStorage(storage).getUserIdMappingForSuperTokensIds(userIds); + return StorageUtils.getUserIdMappingStorage(storage).getUserIdMappingForSuperTokensIds(appIdentifier, userIds); } @TestOnly @@ -350,7 +351,8 @@ public static HashMap getUserIdMappingForSuperTokensUserIds(Main ArrayList userIds) throws StorageQueryException { Storage storage = StorageLayer.getStorage(main); - return getUserIdMappingForSuperTokensUserIds(storage, userIds); + return getUserIdMappingForSuperTokensUserIds( + new AppIdentifier(null, null), storage, userIds); } public static List findNonAuthStoragesWhereUserIdIsUsedOrAssertIfUsed( @@ -422,7 +424,8 @@ public static List findNonAuthStoragesWhereUserIdIsUsedOrAssertIfUsed( return result; } - public static void populateExternalUserIdForUsers(Storage storage, AuthRecipeUserInfo[] users) + public static void populateExternalUserIdForUsers(AppIdentifier appIdentifier, Storage storage, + AuthRecipeUserInfo[] users) throws StorageQueryException { Set userIds = new HashSet<>(); @@ -435,7 +438,8 @@ public static void populateExternalUserIdForUsers(Storage storage, AuthRecipeUse } ArrayList userIdsList = new ArrayList<>(userIds); userIdsList.addAll(userIds); - HashMap userIdMappings = getUserIdMappingForSuperTokensUserIds(storage, userIdsList); + HashMap userIdMappings = getUserIdMappingForSuperTokensUserIds(appIdentifier, storage, + userIdsList); for (AuthRecipeUserInfo user : users) { user.setExternalUserId(userIdMappings.get(user.getSupertokensUserId())); diff --git a/src/main/java/io/supertokens/utils/SemVer.java b/src/main/java/io/supertokens/utils/SemVer.java index 64b63ace6..e02de95fb 100644 --- a/src/main/java/io/supertokens/utils/SemVer.java +++ b/src/main/java/io/supertokens/utils/SemVer.java @@ -34,6 +34,7 @@ public class SemVer implements Comparable { 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"); + public static final SemVer v5_0 = new SemVer("5.0"); final private String version; diff --git a/src/main/java/io/supertokens/utils/Utils.java b/src/main/java/io/supertokens/utils/Utils.java index ca66eaa63..ecd3a0479 100644 --- a/src/main/java/io/supertokens/utils/Utils.java +++ b/src/main/java/io/supertokens/utils/Utils.java @@ -312,6 +312,10 @@ public static boolean verifyWithPublicKey(String content, String signature, Stri return sign.verify(decoder.decode(signature)); } + public static boolean isFakeEmail(String email) { + return email.endsWith("@stfakeemail.supertokens.com") || email.endsWith(".fakeemail.com"); + } + public static class PubPriKey { public String publicKey; public String privateKey; diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index 104121d72..700fb4ba1 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -187,6 +187,7 @@ private void setupRoutes() { addAPI(new DeleteCodesAPI(main)); addAPI(new DeleteCodeAPI(main)); addAPI(new CreateCodeAPI(main)); + addAPI(new CheckCodeAPI(main)); addAPI(new ConsumeCodeAPI(main)); addAPI(new TelemetryAPI(main)); addAPI(new UsersCountAPI(main)); @@ -216,6 +217,7 @@ private void setupRoutes() { addAPI(new VerifyTotpAPI(main)); addAPI(new RemoveTotpDeviceAPI(main)); addAPI(new GetTotpDevicesAPI(main)); + addAPI(new ImportTotpDeviceAPI(main)); addAPI(new UpdateExternalUserIdInfoAPI(main)); addAPI(new ImportUserWithPasswordHashAPI(main)); addAPI(new LicenseKeyAPI(main)); diff --git a/src/main/java/io/supertokens/webserver/WebserverAPI.java b/src/main/java/io/supertokens/webserver/WebserverAPI.java index 5c9841618..616413635 100644 --- a/src/main/java/io/supertokens/webserver/WebserverAPI.java +++ b/src/main/java/io/supertokens/webserver/WebserverAPI.java @@ -73,10 +73,11 @@ public abstract class WebserverAPI extends HttpServlet { supportedVersions.add(SemVer.v2_21); supportedVersions.add(SemVer.v3_0); supportedVersions.add(SemVer.v4_0); + supportedVersions.add(SemVer.v5_0); } public static SemVer getLatestCDIVersion() { - return SemVer.v4_0; + return SemVer.v5_0; } public SemVer getLatestCDIVersionForRequest(HttpServletRequest req) @@ -328,6 +329,13 @@ protected Storage getTenantStorage(HttpServletRequest req) protected Storage[] enforcePublicTenantAndGetAllStoragesForApp(HttpServletRequest req) throws ServletException, BadPermissionException, TenantOrAppNotFoundException { + + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + if (getTenantId(req) != null) { + throw new BadPermissionException("Only public tenantId can call this app specific API"); + } + } + AppIdentifier appIdentifier = getAppIdentifierWithoutVerifying(req); return StorageLayer.getStoragesForApp(main, appIdentifier); } @@ -335,8 +343,13 @@ protected Storage[] enforcePublicTenantAndGetAllStoragesForApp(HttpServletReques protected Storage enforcePublicTenantAndGetPublicTenantStorage( HttpServletRequest req) throws TenantOrAppNotFoundException, BadPermissionException, ServletException { - AppIdentifier appIdentifier = getAppIdentifierWithoutVerifying(req); + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + if (getTenantId(req) != null) { + throw new BadPermissionException("Only public tenantId can call this app specific API"); + } + } + AppIdentifier appIdentifier = getAppIdentifier(req); return StorageLayer.getStorage(appIdentifier.getAsPublicTenantIdentifier(), main); } diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/CanLinkAccountsAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/CanLinkAccountsAPI.java index 75e416eb9..4c5212708 100644 --- a/src/main/java/io/supertokens/webserver/api/accountlinking/CanLinkAccountsAPI.java +++ b/src/main/java/io/supertokens/webserver/api/accountlinking/CanLinkAccountsAPI.java @@ -130,7 +130,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO try { JsonObject response = new JsonObject(); response.addProperty("status", "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); - UserIdMapping.populateExternalUserIdForUsers(recipeUserIdStorage, + UserIdMapping.populateExternalUserIdForUsers(appIdentifier, recipeUserIdStorage, new AuthRecipeUserInfo[]{e.recipeUser}); response.addProperty("primaryUserId", e.recipeUser.getSupertokensOrExternalUserId()); response.addProperty("description", e.getMessage()); diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java index 0fa6fbb7f..80caf2ff3 100644 --- a/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java +++ b/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java @@ -17,6 +17,7 @@ package io.supertokens.webserver.api.accountlinking; import com.google.gson.JsonObject; +import io.supertokens.ActiveUsers; import io.supertokens.Main; import io.supertokens.StorageAndUserIdMapping; import io.supertokens.authRecipe.AuthRecipe; @@ -105,11 +106,22 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I appIdentifier, primaryUserIdStorage, recipeUserId, primaryUserId); - UserIdMapping.populateExternalUserIdForUsers(primaryUserIdStorage, new AuthRecipeUserInfo[]{linkAccountsResult.user}); + UserIdMapping.populateExternalUserIdForUsers(appIdentifier, primaryUserIdStorage, + new AuthRecipeUserInfo[]{linkAccountsResult.user}); JsonObject response = new JsonObject(); response.addProperty("status", "OK"); response.addProperty("accountsAlreadyLinked", linkAccountsResult.wasAlreadyLinked); response.add("user", linkAccountsResult.user.toJson()); + + if (!linkAccountsResult.wasAlreadyLinked) { + try { + ActiveUsers.updateLastActiveAfterLinking( + main, appIdentifier, primaryUserId, recipeUserId); + } catch (Exception e) { + // ignore + } + } + super.sendJsonResponse(200, response, resp); } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException | BadPermissionException e) { @@ -137,7 +149,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { JsonObject response = new JsonObject(); response.addProperty("status", "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"); - UserIdMapping.populateExternalUserIdForUsers(recipeUserIdStorage, new AuthRecipeUserInfo[]{e.recipeUser}); + UserIdMapping.populateExternalUserIdForUsers(appIdentifier, recipeUserIdStorage, + new AuthRecipeUserInfo[]{e.recipeUser}); response.addProperty("primaryUserId", e.recipeUser.getSupertokensOrExternalUserId()); response.addProperty("description", e.getMessage()); response.add("user", e.recipeUser.toJson()); diff --git a/src/main/java/io/supertokens/webserver/api/core/GetUserByIdAPI.java b/src/main/java/io/supertokens/webserver/api/core/GetUserByIdAPI.java index a09155701..37d956ce5 100644 --- a/src/main/java/io/supertokens/webserver/api/core/GetUserByIdAPI.java +++ b/src/main/java/io/supertokens/webserver/api/core/GetUserByIdAPI.java @@ -71,7 +71,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO // if a userIdMapping exists, set the userId in the response to the externalUserId if (user != null) { UserIdMapping.populateExternalUserIdForUsers( - storageAndUserIdMapping.storage, new AuthRecipeUserInfo[]{user}); + appIdentifier, storageAndUserIdMapping.storage, new AuthRecipeUserInfo[]{user}); } } catch (UnknownUserIdException 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 index 4c002bed2..00dd0123f 100644 --- a/src/main/java/io/supertokens/webserver/api/core/ListUsersByAccountInfoAPI.java +++ b/src/main/java/io/supertokens/webserver/api/core/ListUsersByAccountInfoAPI.java @@ -78,7 +78,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO AuthRecipeUserInfo[] users = AuthRecipe.getUsersByAccountInfo( tenantIdentifier, storage, doUnionOfAccountInfo, email, phoneNumber, thirdPartyId, thirdPartyUserId); - UserIdMapping.populateExternalUserIdForUsers(storage, users); + UserIdMapping.populateExternalUserIdForUsers(tenantIdentifier.toAppIdentifier(), storage, users); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); 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 2959ca24a..46b45ea30 100644 --- a/src/main/java/io/supertokens/webserver/api/core/UsersAPI.java +++ b/src/main/java/io/supertokens/webserver/api/core/UsersAPI.java @@ -170,7 +170,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO limit, timeJoinedOrder, paginationToken, recipeIdsEnumBuilder.build().toArray(RECIPE_ID[]::new), searchTags); - UserIdMapping.populateExternalUserIdForUsers(storage, users.users); + UserIdMapping.populateExternalUserIdForUsers(tenantIdentifier.toAppIdentifier(), storage, users.users); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); 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 3d05fd5ff..30857d18d 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/ImportUserWithPasswordHashAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/ImportUserWithPasswordHashAPI.java @@ -100,7 +100,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I EmailPassword.ImportUserResponse importUserResponse = EmailPassword.importUserWithPasswordHash( tenantIdentifier, storage, main, email, passwordHash, passwordHashingAlgorithm); - UserIdMapping.populateExternalUserIdForUsers(storage, new AuthRecipeUserInfo[]{importUserResponse.user}); + UserIdMapping.populateExternalUserIdForUsers(tenantIdentifier.toAppIdentifier(), storage, + new AuthRecipeUserInfo[]{importUserResponse.user}); JsonObject response = new JsonObject(); response.addProperty("status", "OK"); JsonObject userJson = 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 f966180f0..4cdd051af 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/SignInAPI.java @@ -21,6 +21,7 @@ import io.supertokens.Main; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.emailpassword.exceptions.WrongCredentialsException; +import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.output.Logging; import io.supertokens.pluginInterface.RECIPE_ID; @@ -78,8 +79,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { AuthRecipeUserInfo user = EmailPassword.signIn(tenantIdentifier, storage, super.main, normalisedEmail, password); - io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(storage, - new AuthRecipeUserInfo[]{user}); + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers( + tenantIdentifier.toAppIdentifier(), storage, new AuthRecipeUserInfo[]{user}); ActiveUsers.updateLastActive(tenantIdentifier.toAppIdentifier(), main, user.getSupertokensUserId()); // use the internal user id 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 4cf605bf1..01217f817 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/SignUpAPI.java @@ -20,6 +20,7 @@ import io.supertokens.ActiveUsers; import io.supertokens.Main; import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.output.Logging; import io.supertokens.pluginInterface.RECIPE_ID; @@ -100,6 +101,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I 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)); 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 f7b67e729..6f1c56ae9 100644 --- a/src/main/java/io/supertokens/webserver/api/emailpassword/UserAPI.java +++ b/src/main/java/io/supertokens/webserver/api/emailpassword/UserAPI.java @@ -95,7 +95,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO appIdentifier, storageAndUserIdMapping.storage, userId); if (user != null) { - UserIdMapping.populateExternalUserIdForUsers(storageAndUserIdMapping.storage, + UserIdMapping.populateExternalUserIdForUsers(appIdentifier, + storageAndUserIdMapping.storage, new AuthRecipeUserInfo[]{user}); } @@ -109,7 +110,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO // if a userIdMapping exists, set the userId in the response to the externalUserId if (user != null) { - UserIdMapping.populateExternalUserIdForUsers(storage, new AuthRecipeUserInfo[]{user}); + UserIdMapping.populateExternalUserIdForUsers(appIdentifier, storage, + new AuthRecipeUserInfo[]{user}); } } } catch (UnknownUserIdException e) { diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/BaseCreateOrUpdate.java b/src/main/java/io/supertokens/webserver/api/multitenancy/BaseCreateOrUpdate.java index 87bc319b5..2fbfb5ce4 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/BaseCreateOrUpdate.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/BaseCreateOrUpdate.java @@ -47,10 +47,20 @@ public BaseCreateOrUpdate(Main main) { protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdentifier, TenantIdentifier targetTenantIdentifier, Boolean emailPasswordEnabled, - Boolean thirdPartyEnabled, Boolean passwordlessEnabled, JsonObject coreConfig, - HttpServletResponse resp) + Boolean thirdPartyEnabled, Boolean passwordlessEnabled, + boolean hasFirstFactors, String[] firstFactors, + boolean hasRequiredSecondaryFactors, String[] requiredSecondaryFactors, + JsonObject coreConfig, HttpServletResponse resp) throws ServletException, IOException { + if (hasFirstFactors && firstFactors != null && firstFactors.length == 0) { + throw new ServletException(new BadRequestException("firstFactors cannot be empty. Set null instead to remove all first factors.")); + } + + if (hasRequiredSecondaryFactors && requiredSecondaryFactors != null && requiredSecondaryFactors.length == 0) { + throw new ServletException(new BadRequestException("requiredSecondaryFactors cannot be empty. Set null instead to remove all required secondary factors.")); + } + CoreConfig baseConfig = Config.getBaseConfig(main); if (baseConfig.getSuperTokensLoadOnlyCUD() != null) { if (!(targetTenantIdentifier.getConnectionUriDomain().equals(TenantIdentifier.DEFAULT_CONNECTION_URI) || targetTenantIdentifier.getConnectionUriDomain().equals(baseConfig.getSuperTokensLoadOnlyCUD()))) { @@ -73,7 +83,7 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ); } else { // We disable all recipes by default while creating tenant @@ -82,7 +92,7 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ); } createdNew = true; @@ -94,7 +104,7 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent new EmailPasswordConfig(emailPasswordEnabled), tenantConfig.thirdPartyConfig, tenantConfig.passwordlessConfig, - tenantConfig.coreConfig + tenantConfig.firstFactors, tenantConfig.requiredSecondaryFactors, tenantConfig.coreConfig ); } @@ -104,7 +114,7 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent tenantConfig.emailPasswordConfig, new ThirdPartyConfig(thirdPartyEnabled, tenantConfig.thirdPartyConfig.providers), tenantConfig.passwordlessConfig, - tenantConfig.coreConfig + tenantConfig.firstFactors, tenantConfig.requiredSecondaryFactors, tenantConfig.coreConfig ); } @@ -114,7 +124,27 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent tenantConfig.emailPasswordConfig, tenantConfig.thirdPartyConfig, new PasswordlessConfig(passwordlessEnabled), - tenantConfig.coreConfig + tenantConfig.firstFactors, tenantConfig.requiredSecondaryFactors, tenantConfig.coreConfig + ); + } + + if (hasFirstFactors) { + tenantConfig = new TenantConfig( + tenantConfig.tenantIdentifier, + tenantConfig.emailPasswordConfig, + tenantConfig.thirdPartyConfig, + tenantConfig.passwordlessConfig, + firstFactors, tenantConfig.requiredSecondaryFactors, tenantConfig.coreConfig + ); + } + + if (hasRequiredSecondaryFactors) { + tenantConfig = new TenantConfig( + tenantConfig.tenantIdentifier, + tenantConfig.emailPasswordConfig, + tenantConfig.thirdPartyConfig, + tenantConfig.passwordlessConfig, + tenantConfig.firstFactors, requiredSecondaryFactors, tenantConfig.coreConfig ); } @@ -125,7 +155,7 @@ protected void handle(HttpServletRequest req, TenantIdentifier sourceTenantIdent tenantConfig.emailPasswordConfig, tenantConfig.thirdPartyConfig, tenantConfig.passwordlessConfig, - coreConfig + tenantConfig.firstFactors, tenantConfig.requiredSecondaryFactors, coreConfig ); } diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateAppAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateAppAPI.java index 6fef2bd4a..e58343a28 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateAppAPI.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateAppAPI.java @@ -16,10 +16,12 @@ package io.supertokens.webserver.api.multitenancy; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.Utils; import jakarta.servlet.ServletException; @@ -27,6 +29,8 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; public class CreateOrUpdateAppAPI extends BaseCreateOrUpdate { @@ -54,6 +58,36 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO Boolean passwordlessEnabled = InputParser.parseBooleanOrThrowError(input, "passwordlessEnabled", true); JsonObject coreConfig = InputParser.parseJsonObjectOrThrowError(input, "coreConfig", true); + String[] firstFactors = null; + boolean hasFirstFactors = false; + String[] requiredSecondaryFactors = null; + boolean hasRequiredSecondaryFactors = false; + + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + hasFirstFactors = input.has("firstFactors"); + if (hasFirstFactors && !input.get("firstFactors").isJsonNull()) { + JsonArray firstFactorsArr = InputParser.parseArrayOrThrowError(input, "firstFactors", true); + firstFactors = new String[firstFactorsArr.size()]; + for (int i = 0; i < firstFactors.length; i++) { + firstFactors[i] = InputParser.parseStringFromElementOrThrowError(firstFactorsArr.get(i), "firstFactors", false); + } + if (firstFactors.length != new HashSet<>(Arrays.asList(firstFactors)).size()) { + throw new ServletException(new BadRequestException("firstFactors input should not contain duplicate values")); + } + } + hasRequiredSecondaryFactors = input.has("requiredSecondaryFactors"); + if (hasRequiredSecondaryFactors && !input.get("requiredSecondaryFactors").isJsonNull()) { + JsonArray requiredSecondaryFactorsArr = InputParser.parseArrayOrThrowError(input, "requiredSecondaryFactors", true); + requiredSecondaryFactors = new String[requiredSecondaryFactorsArr.size()]; + for (int i = 0; i < requiredSecondaryFactors.length; i++) { + requiredSecondaryFactors[i] = InputParser.parseStringFromElementOrThrowError(requiredSecondaryFactorsArr.get(i), "requiredSecondaryFactors", false); + } + if (requiredSecondaryFactors.length != new HashSet<>(Arrays.asList(requiredSecondaryFactors)).size()) { + throw new ServletException(new BadRequestException("requiredSecondaryFactors input should not contain duplicate values")); + } + } + } + TenantIdentifier sourceTenantIdentifier; try { @@ -65,7 +99,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO super.handle( req, sourceTenantIdentifier, new TenantIdentifier(sourceTenantIdentifier.getConnectionUriDomain(), appId, null), - emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled, coreConfig, resp); + emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled, + hasFirstFactors, firstFactors, hasRequiredSecondaryFactors, requiredSecondaryFactors, + coreConfig, resp); } } diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateConnectionUriDomainAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateConnectionUriDomainAPI.java index 2eba982c6..2e2ff217c 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateConnectionUriDomainAPI.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateConnectionUriDomainAPI.java @@ -16,10 +16,12 @@ package io.supertokens.webserver.api.multitenancy; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.Utils; import jakarta.servlet.ServletException; @@ -27,6 +29,8 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; public class CreateOrUpdateConnectionUriDomainAPI extends BaseCreateOrUpdate { @@ -54,6 +58,36 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO Boolean passwordlessEnabled = InputParser.parseBooleanOrThrowError(input, "passwordlessEnabled", true); JsonObject coreConfig = InputParser.parseJsonObjectOrThrowError(input, "coreConfig", true); + String[] firstFactors = null; + boolean hasFirstFactors = false; + String[] requiredSecondaryFactors = null; + boolean hasRequiredSecondaryFactors = false; + + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + hasFirstFactors = input.has("firstFactors"); + if (hasFirstFactors && !input.get("firstFactors").isJsonNull()) { + JsonArray firstFactorsArr = InputParser.parseArrayOrThrowError(input, "firstFactors", true); + firstFactors = new String[firstFactorsArr.size()]; + for (int i = 0; i < firstFactors.length; i++) { + firstFactors[i] = InputParser.parseStringFromElementOrThrowError(firstFactorsArr.get(i), "firstFactors", false); + } + if (firstFactors.length != new HashSet<>(Arrays.asList(firstFactors)).size()) { + throw new ServletException(new BadRequestException("firstFactors input should not contain duplicate values")); + } + } + hasRequiredSecondaryFactors = input.has("requiredSecondaryFactors"); + if (hasRequiredSecondaryFactors && !input.get("requiredSecondaryFactors").isJsonNull()) { + JsonArray requiredSecondaryFactorsArr = InputParser.parseArrayOrThrowError(input, "requiredSecondaryFactors", true); + requiredSecondaryFactors = new String[requiredSecondaryFactorsArr.size()]; + for (int i = 0; i < requiredSecondaryFactors.length; i++) { + requiredSecondaryFactors[i] = InputParser.parseStringFromElementOrThrowError(requiredSecondaryFactorsArr.get(i), "requiredSecondaryFactors", false); + } + if (requiredSecondaryFactors.length != new HashSet<>(Arrays.asList(requiredSecondaryFactors)).size()) { + throw new ServletException(new BadRequestException("requiredSecondaryFactors input should not contain duplicate values")); + } + } + } + TenantIdentifier sourceTenantIdentifier; try { sourceTenantIdentifier = getTenantIdentifier(req); @@ -64,7 +98,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO super.handle( req, sourceTenantIdentifier, new TenantIdentifier(connectionUriDomain, null, null), - emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled, coreConfig, resp); + emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled, + hasFirstFactors, firstFactors, hasRequiredSecondaryFactors, requiredSecondaryFactors, + coreConfig, resp); } } diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateTenantOrGetTenantAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateTenantOrGetTenantAPI.java index 63998b615..db8ab6079 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateTenantOrGetTenantAPI.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateTenantOrGetTenantAPI.java @@ -16,12 +16,14 @@ package io.supertokens.webserver.api.multitenancy; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.config.CoreConfig; import io.supertokens.multitenancy.Multitenancy; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.Utils; import jakarta.servlet.ServletException; @@ -29,6 +31,8 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; public class CreateOrUpdateTenantOrGetTenantAPI extends BaseCreateOrUpdate { @@ -57,6 +61,36 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO Boolean passwordlessEnabled = InputParser.parseBooleanOrThrowError(input, "passwordlessEnabled", true); JsonObject coreConfig = InputParser.parseJsonObjectOrThrowError(input, "coreConfig", true); + String[] firstFactors = null; + boolean hasFirstFactors = false; + String[] requiredSecondaryFactors = null; + boolean hasRequiredSecondaryFactors = false; + + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + hasFirstFactors = input.has("firstFactors"); + if (hasFirstFactors && !input.get("firstFactors").isJsonNull()) { + JsonArray firstFactorsArr = InputParser.parseArrayOrThrowError(input, "firstFactors", true); + firstFactors = new String[firstFactorsArr.size()]; + for (int i = 0; i < firstFactors.length; i++) { + firstFactors[i] = InputParser.parseStringFromElementOrThrowError(firstFactorsArr.get(i), "firstFactors", false); + } + if (firstFactors.length != new HashSet<>(Arrays.asList(firstFactors)).size()) { + throw new ServletException(new BadRequestException("firstFactors input should not contain duplicate values")); + } + } + hasRequiredSecondaryFactors = input.has("requiredSecondaryFactors"); + if (hasRequiredSecondaryFactors && !input.get("requiredSecondaryFactors").isJsonNull()) { + JsonArray requiredSecondaryFactorsArr = InputParser.parseArrayOrThrowError(input, "requiredSecondaryFactors", true); + requiredSecondaryFactors = new String[requiredSecondaryFactorsArr.size()]; + for (int i = 0; i < requiredSecondaryFactors.length; i++) { + requiredSecondaryFactors[i] = InputParser.parseStringFromElementOrThrowError(requiredSecondaryFactorsArr.get(i), "requiredSecondaryFactors", false); + } + if (requiredSecondaryFactors.length != new HashSet<>(Arrays.asList(requiredSecondaryFactors)).size()) { + throw new ServletException(new BadRequestException("requiredSecondaryFactors input should not contain duplicate values")); + } + } + } + TenantIdentifier sourceTenantIdentifier; try { sourceTenantIdentifier = getTenantIdentifier(req); @@ -67,8 +101,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO super.handle( req, sourceTenantIdentifier, new TenantIdentifier(sourceTenantIdentifier.getConnectionUriDomain(), sourceTenantIdentifier.getAppId(), tenantId), - emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled, coreConfig, resp); - + emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled, + hasFirstFactors, firstFactors, hasRequiredSecondaryFactors, requiredSecondaryFactors, + coreConfig, resp); } @Override @@ -83,6 +118,11 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject result = config.toJson(shouldProtect, getTenantStorage(req), CoreConfig.PROTECTED_CONFIGS); result.addProperty("status", "OK"); + if (getVersionFromRequest(req).lesserThan(SemVer.v5_0)) { + result.remove("firstFactors"); + result.remove("requiredSecondaryFactors"); + } + super.sendJsonResponse(200, result, resp); } catch (TenantOrAppNotFoundException e) { JsonObject result = new JsonObject(); diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/ListTenantsAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/ListTenantsAPI.java index ae782f8b8..b2fe19ef6 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/ListTenantsAPI.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/ListTenantsAPI.java @@ -52,7 +52,6 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO TenantIdentifier tenantIdentifier = getTenantIdentifier(req); Storage storage = getTenantStorage(req); - enforcePublicTenantAndGetPublicTenantStorage(req); // enforce that this API is called using public tenant if (!tenantIdentifier.getTenantId().equals(TenantIdentifier.DEFAULT_TENANT_ID)) { throw new BadPermissionException("Only the public tenantId is allowed to list all tenants " + "associated with this app"); diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/RemoveTenantAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/RemoveTenantAPI.java index b5d251fb9..b4faa4cd3 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/RemoveTenantAPI.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/RemoveTenantAPI.java @@ -53,8 +53,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String tenantId = InputParser.parseStringOrThrowError(input, "tenantId", false); tenantId = Utils.normalizeAndValidateTenantId(tenantId); - - if (tenantId.equals(TenantIdentifier.DEFAULT_TENANT_ID)) { throw new ServletException(new BadPermissionException("Cannot delete public tenant, use remove app API instead")); } @@ -62,7 +60,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I try { TenantIdentifier sourceTenantIdentifier = this.getTenantIdentifier(req); - enforcePublicTenantAndGetPublicTenantStorage(req); // Enforce public tenant if (!sourceTenantIdentifier.getTenantId().equals(TenantIdentifier.DEFAULT_TENANT_ID)) { throw new BadPermissionException("Only the public tenantId is allowed to delete a tenant"); } diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/CreateOrUpdateThirdPartyConfigAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/CreateOrUpdateThirdPartyConfigAPI.java index 18eff9b7c..a63a8a7c0 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/CreateOrUpdateThirdPartyConfigAPI.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/CreateOrUpdateThirdPartyConfigAPI.java @@ -114,7 +114,8 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO tenantConfig.thirdPartyConfig.enabled, newProviders.toArray(new ThirdPartyConfig.Provider[0])), tenantConfig.passwordlessConfig, - tenantConfig.coreConfig); + tenantConfig.firstFactors, tenantConfig.requiredSecondaryFactors, tenantConfig.coreConfig + ); Multitenancy.addNewOrUpdateAppOrTenant(main, updatedConfig, shouldProtectProtectedConfig(req), skipValidation, true); diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/RemoveThirdPartyConfigAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/RemoveThirdPartyConfigAPI.java index 3fe12ea24..c92a514c0 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/RemoveThirdPartyConfigAPI.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/RemoveThirdPartyConfigAPI.java @@ -83,7 +83,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I new ThirdPartyConfig( config.thirdPartyConfig.enabled, newProviders.toArray(new ThirdPartyConfig.Provider[0])), config.passwordlessConfig, - config.coreConfig); + config.firstFactors, config.requiredSecondaryFactors, config.coreConfig + ); Multitenancy.addNewOrUpdateAppOrTenant(main, updatedConfig, shouldProtectProtectedConfig(req), false, true); diff --git a/src/main/java/io/supertokens/webserver/api/passwordless/CheckCodeAPI.java b/src/main/java/io/supertokens/webserver/api/passwordless/CheckCodeAPI.java new file mode 100644 index 000000000..0ff536995 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/passwordless/CheckCodeAPI.java @@ -0,0 +1,134 @@ +/* + * 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.passwordless; + +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.*; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; +import io.supertokens.utils.SemVer; +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.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +public class CheckCodeAPI extends WebserverAPI { + + private static final long serialVersionUID = -4641988458637882374L; + + public CheckCodeAPI(Main main) { + super(main, RECIPE_ID.PASSWORDLESS.toString()); + } + + @Override + public String getPath() { + return "/recipe/signinup/code/check"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // API is tenant specific + // Logic based on: https://app.code2flow.com/OFxcbh1FNLXd + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + String linkCode = null; + String deviceId = null; + String userInputCode = null; + + String deviceIdHash = InputParser.parseStringOrThrowError(input, "preAuthSessionId", false); + + if (input.has("linkCode")) { + if (input.has("userInputCode") || input.has("deviceId")) { + throw new ServletException( + new BadRequestException("Please provide exactly one of linkCode or deviceId+userInputCode")); + } + linkCode = InputParser.parseStringOrThrowError(input, "linkCode", false); + } else if (input.has("userInputCode") && input.has("deviceId")) { + deviceId = InputParser.parseStringOrThrowError(input, "deviceId", false); + userInputCode = InputParser.parseStringOrThrowError(input, "userInputCode", false); + } else { + throw new ServletException( + new BadRequestException("Please provide exactly one of linkCode or deviceId+userInputCode")); + } + + try { + TenantIdentifier tenantIdentifier = getTenantIdentifier(req); + Storage storage = this.getTenantStorage(req); + PasswordlessDevice consumedDevice = Passwordless.checkCodeAndReturnDevice( + tenantIdentifier, + storage, main, + deviceId, deviceIdHash, + userInputCode, linkCode, false); + + JsonObject result = new JsonObject(); + result.addProperty("status", "OK"); + + JsonObject jsonDevice = new JsonObject(); + jsonDevice.addProperty("preAuthSessionId", consumedDevice.deviceIdHash); + jsonDevice.addProperty("failedCodeInputAttemptCount", consumedDevice.failedAttempts); + + if (consumedDevice.email != null) { + jsonDevice.addProperty("email", consumedDevice.email); + } + + if (consumedDevice.phoneNumber != null) { + jsonDevice.addProperty("phoneNumber", consumedDevice.phoneNumber); + } + + result.add("consumedDevice", jsonDevice); + + super.sendJsonResponse(200, result, resp); + } catch (RestartFlowException ex) { + JsonObject result = new JsonObject(); + result.addProperty("status", "RESTART_FLOW_ERROR"); + super.sendJsonResponse(200, result, resp); + } catch (ExpiredUserInputCodeException ex) { + JsonObject result = new JsonObject(); + result.addProperty("status", "EXPIRED_USER_INPUT_CODE_ERROR"); + result.addProperty("failedCodeInputAttemptCount", ex.failedCodeInputs); + result.addProperty("maximumCodeInputAttempts", ex.maximumCodeInputAttempts); + super.sendJsonResponse(200, result, resp); + } catch (IncorrectUserInputCodeException ex) { + JsonObject result = new JsonObject(); + result.addProperty("status", "INCORRECT_USER_INPUT_CODE_ERROR"); + result.addProperty("failedCodeInputAttemptCount", ex.failedCodeInputs); + result.addProperty("maximumCodeInputAttempts", ex.maximumCodeInputAttempts); + + 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) { + throw new ServletException(e); + } catch (Base64EncodingException ex) { + throw new ServletException(new BadRequestException("Input encoding error in " + ex.source)); + } + } +} \ No newline at end of file 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 a1aa41ee6..db22f732b 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java @@ -92,7 +92,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I 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(storage, new AuthRecipeUserInfo[]{consumeCodeResponse.user}); + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers( + tenantIdentifier.toAppIdentifier(), storage, + new AuthRecipeUserInfo[]{consumeCodeResponse.user}); ActiveUsers.updateLastActive(tenantIdentifier.toAppIdentifier(), main, consumeCodeResponse.user.getSupertokensUserId()); @@ -120,6 +122,22 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } } + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + JsonObject jsonDevice = new JsonObject(); + jsonDevice.addProperty("preAuthSessionId", consumeCodeResponse.consumedDevice.deviceIdHash); + jsonDevice.addProperty("failedCodeInputAttemptCount", consumeCodeResponse.consumedDevice.failedAttempts); + + if (consumeCodeResponse.consumedDevice.email != null) { + jsonDevice.addProperty("email", consumeCodeResponse.consumedDevice.email); + } + + if (consumeCodeResponse.consumedDevice.phoneNumber != null) { + jsonDevice.addProperty("phoneNumber", consumeCodeResponse.consumedDevice.phoneNumber); + } + + result.add("consumedDevice", jsonDevice); + } + super.sendJsonResponse(200, result, resp); } catch (RestartFlowException ex) { JsonObject result = new JsonObject(); diff --git a/src/main/java/io/supertokens/webserver/api/passwordless/DeleteCodeAPI.java b/src/main/java/io/supertokens/webserver/api/passwordless/DeleteCodeAPI.java index ea3b76fce..817b68faf 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/DeleteCodeAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/DeleteCodeAPI.java @@ -23,6 +23,7 @@ import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -50,10 +51,29 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I // Logic based on: https://app.code2flow.com/DDhe9U1rsFsQ JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - String codeId = InputParser.parseStringOrThrowError(input, "codeId", false); + String codeId = InputParser.parseStringOrThrowError( + input, "codeId", getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)); + String deviceIdHash = null; + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + deviceIdHash = InputParser.parseStringOrThrowError(input, "preAuthSessionId", true); + } + + if (codeId == null && deviceIdHash == null) { + throw new ServletException(new BadRequestException("Please provide either 'codeId' or 'preAuthSessionId'")); + } + + if (codeId != null && deviceIdHash != null) { + throw new ServletException(new BadRequestException("Please provide only one of 'codeId' or " + + "'preAuthSessionId'")); + } try { - Passwordless.removeCode(getTenantIdentifier(req), getTenantStorage(req), codeId); + if (codeId != null) { + Passwordless.removeCode(getTenantIdentifier(req), getTenantStorage(req), codeId); + } else { + Passwordless.removeDevice(getTenantIdentifier(req), getTenantStorage(req), + deviceIdHash); + } JsonObject result = new JsonObject(); result.addProperty("status", "OK"); 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 430743262..05f4d4786 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java @@ -92,7 +92,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO // if the userIdMapping exists set the userId in the response to the externalUserId if (user != null) { io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers( - storageAndUserIdMapping.storage, new AuthRecipeUserInfo[]{user}); + appIdentifier, storageAndUserIdMapping.storage, new AuthRecipeUserInfo[]{user}); } } catch (UnknownUserIdException e) { user = null; @@ -103,7 +103,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO email = Utils.normaliseEmail(email); user = Passwordless.getUserByEmail(tenantIdentifier, storage, email); if (user != null) { - io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(storage, + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers( + tenantIdentifier.toAppIdentifier(), storage, new AuthRecipeUserInfo[]{user}); } } else { @@ -112,7 +113,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO user = Passwordless.getUserByPhoneNumber(tenantIdentifier, storage, phoneNumber); if (user != null) { - io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(storage, + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers( + tenantIdentifier.toAppIdentifier(), storage, new AuthRecipeUserInfo[]{user}); } } 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 dca193196..81e8e306f 100644 --- a/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java @@ -62,16 +62,18 @@ public String getPath() { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + SemVer version = super.getVersionFromRequest(req); + // API is app specific, but session is updated based on tenantId obtained from the refreshToken JsonObject input = InputParser.parseJsonObjectOrThrowError(req); String refreshToken = InputParser.parseStringOrThrowError(input, "refreshToken", false); String antiCsrfToken = InputParser.parseStringOrThrowError(input, "antiCsrfToken", true); Boolean enableAntiCsrf = InputParser.parseBooleanOrThrowError(input, "enableAntiCsrf", false); + Boolean useDynamicSigningKey = version.greaterThanOrEqualTo(SemVer.v5_0) ? + InputParser.parseBooleanOrThrowError(input, "useDynamicSigningKey", false) : null; assert enableAntiCsrf != null; assert refreshToken != null; - - SemVer version = super.getVersionFromRequest(req); TenantIdentifier tenantIdentifierForLogging = null; try { tenantIdentifierForLogging = getTenantIdentifier(req); @@ -85,7 +87,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I SessionInformationHolder sessionInfo = Session.refreshSession(appIdentifier, main, refreshToken, antiCsrfToken, - enableAntiCsrf, accessTokenVersion); + enableAntiCsrf, accessTokenVersion, + useDynamicSigningKey == null ? null : Boolean.FALSE.equals(useDynamicSigningKey)); TenantIdentifier tenantIdentifier = new TenantIdentifier(appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(), sessionInfo.session.tenantId); Storage storage = StorageLayer.getStorage(tenantIdentifier, main); 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 66fcc8758..45ab7ba41 100644 --- a/src/main/java/io/supertokens/webserver/api/thirdparty/GetUsersByEmailAPI.java +++ b/src/main/java/io/supertokens/webserver/api/thirdparty/GetUsersByEmailAPI.java @@ -61,7 +61,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO String email = InputParser.getQueryParamOrThrowError(req, "email", false); email = Utils.normaliseEmail(email); AuthRecipeUserInfo[] users = ThirdParty.getUsersByEmail(tenantIdentifier, storage, email); - UserIdMapping.populateExternalUserIdForUsers(storage, users); + UserIdMapping.populateExternalUserIdForUsers(tenantIdentifier.toAppIdentifier(), storage, users); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); 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 42b16c7f9..70f5d633b 100644 --- a/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/thirdparty/SignInUpAPI.java @@ -20,6 +20,7 @@ import io.supertokens.ActiveUsers; 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; @@ -81,7 +82,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I ThirdParty.SignInUpResponse response = ThirdParty.signInUp2_7( tenantIdentifier, storage, thirdPartyId, thirdPartyUserId, email, isEmailVerified); - UserIdMapping.populateExternalUserIdForUsers(storage, new AuthRecipeUserInfo[]{response.user}); + UserIdMapping.populateExternalUserIdForUsers(tenantIdentifier.toAppIdentifier(), storage, + new AuthRecipeUserInfo[]{response.user}); ActiveUsers.updateLastActive(tenantIdentifier.toAppIdentifier(), main, response.user.getSupertokensUserId()); @@ -106,6 +108,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } } } + super.sendJsonResponse(200, result, resp); } catch (StorageQueryException | TenantOrAppNotFoundException e) { @@ -142,7 +145,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I ThirdParty.SignInUpResponse response = ThirdParty.signInUp( tenantIdentifier, storage, super.main, thirdPartyId, thirdPartyUserId, email, isEmailVerified); - UserIdMapping.populateExternalUserIdForUsers(storage, new AuthRecipeUserInfo[]{response.user}); + UserIdMapping.populateExternalUserIdForUsers(tenantIdentifier.toAppIdentifier(), storage, + new AuthRecipeUserInfo[]{response.user}); ActiveUsers.updateLastActive(tenantIdentifier.toAppIdentifier(), main, response.user.getSupertokensUserId()); @@ -169,6 +173,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } } } + super.sendJsonResponse(200, result, resp); } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { 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 f1089b011..1d0ec397c 100644 --- a/src/main/java/io/supertokens/webserver/api/thirdparty/UserAPI.java +++ b/src/main/java/io/supertokens/webserver/api/thirdparty/UserAPI.java @@ -91,7 +91,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO user = ThirdParty.getUser(appIdentifier, storageAndUserIdMapping.storage, userId); if (user != null) { - UserIdMapping.populateExternalUserIdForUsers(storageAndUserIdMapping.storage, + UserIdMapping.populateExternalUserIdForUsers(appIdentifier, storageAndUserIdMapping.storage, new AuthRecipeUserInfo[]{user}); } } catch (UnknownUserIdException e) { @@ -103,7 +103,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO user = ThirdParty.getUser(tenantIdentifier, storage, thirdPartyId, thirdPartyUserId); if (user != null) { - UserIdMapping.populateExternalUserIdForUsers(storage, new AuthRecipeUserInfo[]{user}); + UserIdMapping.populateExternalUserIdForUsers(tenantIdentifier.toAppIdentifier(), storage, + new AuthRecipeUserInfo[]{user}); } } diff --git a/src/main/java/io/supertokens/webserver/api/totp/CreateOrUpdateTotpDeviceAPI.java b/src/main/java/io/supertokens/webserver/api/totp/CreateOrUpdateTotpDeviceAPI.java index 59b9f0fa7..1243bec42 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/CreateOrUpdateTotpDeviceAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/CreateOrUpdateTotpDeviceAPI.java @@ -13,7 +13,6 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.totp.TOTPDevice; import io.supertokens.pluginInterface.totp.exception.DeviceAlreadyExistsException; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; import io.supertokens.totp.Totp; import io.supertokens.useridmapping.UserIdType; @@ -44,7 +43,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject input = InputParser.parseJsonObjectOrThrowError(req); String userId = InputParser.parseStringOrThrowError(input, "userId", false); - String deviceName = InputParser.parseStringOrThrowError(input, "deviceName", false); + String deviceName = InputParser.parseStringOrThrowError(input, "deviceName", true); Integer skew = InputParser.parseIntOrThrowError(input, "skew", false); Integer period = InputParser.parseIntOrThrowError(input, "period", false); @@ -54,7 +53,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (userId.isEmpty()) { throw new ServletException(new BadRequestException("userId cannot be empty")); } - if (deviceName.isEmpty()) { + if (deviceName != null && deviceName.isEmpty()) { + // Only Null or valid device name are allowed throw new ServletException(new BadRequestException("deviceName cannot be empty")); } if (skew < 0) { @@ -84,6 +84,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I TOTPDevice device = Totp.registerDevice(appIdentifier, storage, main, userId, deviceName, skew, period); result.addProperty("status", "OK"); + result.addProperty("deviceName", device.deviceName); result.addProperty("secret", device.secretKey); super.sendJsonResponse(200, result, resp); } catch (DeviceAlreadyExistsException e) { @@ -137,9 +138,6 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO result.addProperty("status", "OK"); super.sendJsonResponse(200, result, resp); - } catch (TotpNotEnabledException e) { - result.addProperty("status", "TOTP_NOT_ENABLED_ERROR"); - super.sendJsonResponse(200, result, resp); } catch (UnknownDeviceException e) { result.addProperty("status", "UNKNOWN_DEVICE_ERROR"); super.sendJsonResponse(200, result, resp); diff --git a/src/main/java/io/supertokens/webserver/api/totp/GetTotpDevicesAPI.java b/src/main/java/io/supertokens/webserver/api/totp/GetTotpDevicesAPI.java index 9636a9590..0f7c86d59 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/GetTotpDevicesAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/GetTotpDevicesAPI.java @@ -12,7 +12,6 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.totp.TOTPDevice; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; import io.supertokens.totp.Totp; import io.supertokens.useridmapping.UserIdType; import io.supertokens.webserver.InputParser; @@ -77,9 +76,6 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO result.addProperty("status", "OK"); result.add("devices", devicesArray); super.sendJsonResponse(200, result, resp); - } catch (TotpNotEnabledException e) { - result.addProperty("status", "TOTP_NOT_ENABLED_ERROR"); - super.sendJsonResponse(200, result, resp); } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { throw new ServletException(e); } diff --git a/src/main/java/io/supertokens/webserver/api/totp/ImportTotpDeviceAPI.java b/src/main/java/io/supertokens/webserver/api/totp/ImportTotpDeviceAPI.java new file mode 100644 index 000000000..22ea7118b --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/totp/ImportTotpDeviceAPI.java @@ -0,0 +1,100 @@ +package io.supertokens.webserver.api.totp; + +import com.google.gson.JsonObject; +import io.supertokens.Main; +import io.supertokens.StorageAndUserIdMapping; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.totp.TOTPDevice; +import io.supertokens.pluginInterface.totp.exception.DeviceAlreadyExistsException; +import io.supertokens.totp.Totp; +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 ImportTotpDeviceAPI extends WebserverAPI { + private static final long serialVersionUID = -4641988458637882374L; + + public ImportTotpDeviceAPI(Main main) { + super(main, RECIPE_ID.TOTP.toString()); + } + + @Override + public String getPath() { + return "/recipe/totp/device/import"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // API is app specific + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + + String userId = InputParser.parseStringOrThrowError(input, "userId", false); + String deviceName = InputParser.parseStringOrThrowError(input, "deviceName", true); + Integer skew = InputParser.parseIntOrThrowError(input, "skew", false); + Integer period = InputParser.parseIntOrThrowError(input, "period", false); + String secretKey = InputParser.parseStringOrThrowError(input, "secretKey", false); + + // Note: Not allowing the user to change the hashing algo and totp + // length (6-8) at the moment because it's rare to change them + + if (userId.isEmpty()) { + throw new ServletException(new BadRequestException("userId cannot be empty")); + } + if (deviceName != null && deviceName.isEmpty()) { + // Only Null or valid device name are allowed + throw new ServletException(new BadRequestException("deviceName cannot be empty")); + } + if (secretKey.isEmpty()) { + throw new ServletException(new BadRequestException("secretKey cannot be empty")); + } + if (skew < 0) { + throw new ServletException(new BadRequestException("skew must be >= 0")); + } + if (period <= 0) { + throw new ServletException(new BadRequestException("period must be > 0")); + } + + JsonObject result = new JsonObject(); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage; + // This step is required only because user_last_active table stores supertokens internal user id. + // While sending the usage stats we do a join, so totp tables also must use internal user id. + + // Try to find the appIdentifier with right storage based on the userId + try { + StorageAndUserIdMapping mappingAndStorage = enforcePublicTenantAndGetStorageAndUserIdMappingForAppSpecificApi( + req, userId, UserIdType.ANY, false); + storage = mappingAndStorage.storage; + } catch (UnknownUserIdException e) { + throw new IllegalStateException("should never happen"); + } + + TOTPDevice createdDevice = Totp.createDevice(super.main, appIdentifier, storage, + userId, deviceName, skew, period, secretKey, true, System.currentTimeMillis()); + + result.addProperty("status", "OK"); + result.addProperty("deviceName", createdDevice.deviceName); + super.sendJsonResponse(200, result, resp); + } catch (DeviceAlreadyExistsException e) { + result.addProperty("status", "DEVICE_ALREADY_EXISTS_ERROR"); + super.sendJsonResponse(200, result, resp); + } catch (StorageQueryException | FeatureNotEnabledException | + TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } +} diff --git a/src/main/java/io/supertokens/webserver/api/totp/RemoveTotpDeviceAPI.java b/src/main/java/io/supertokens/webserver/api/totp/RemoveTotpDeviceAPI.java index cdbbe8bd0..0501d5dfe 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/RemoveTotpDeviceAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/RemoveTotpDeviceAPI.java @@ -11,7 +11,6 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; import io.supertokens.totp.Totp; import io.supertokens.useridmapping.UserIdType; @@ -72,9 +71,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I result.addProperty("status", "OK"); result.addProperty("didDeviceExist", true); super.sendJsonResponse(200, result, resp); - } catch (TotpNotEnabledException e) { - result.addProperty("status", "TOTP_NOT_ENABLED_ERROR"); - super.sendJsonResponse(200, result, resp); } catch (UnknownDeviceException e) { result.addProperty("status", "OK"); result.addProperty("didDeviceExist", false); diff --git a/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpAPI.java b/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpAPI.java index 6489d7922..83b5f16b4 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpAPI.java @@ -10,12 +10,13 @@ import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; -import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; +import io.supertokens.pluginInterface.totp.exception.UnknownTotpUserIdException; import io.supertokens.totp.Totp; import io.supertokens.totp.exceptions.InvalidTotpException; import io.supertokens.totp.exceptions.LimitReachedException; +import io.supertokens.utils.SemVer; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -41,7 +42,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String userId = InputParser.parseStringOrThrowError(input, "userId", false); String totp = InputParser.parseStringOrThrowError(input, "totp", false); - Boolean allowUnverifiedDevices = InputParser.parseBooleanOrThrowError(input, "allowUnverifiedDevices", false); if (userId.isEmpty()) { throw new ServletException(new BadRequestException("userId cannot be empty")); @@ -49,7 +49,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I if (totp.length() != 6) { throw new ServletException(new BadRequestException("totp must be 6 characters long")); } - // Already checked that allowUnverifiedDevices is not null. JsonObject result = new JsonObject(); @@ -57,19 +56,27 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I TenantIdentifier tenantIdentifier = getTenantIdentifier(req); Storage storage = getTenantStorage(req); - Totp.verifyCode(tenantIdentifier, storage, main, userId, totp, allowUnverifiedDevices); + Totp.verifyCode(tenantIdentifier, storage, main, userId, totp); result.addProperty("status", "OK"); super.sendJsonResponse(200, result, resp); - } catch (TotpNotEnabledException e) { - result.addProperty("status", "TOTP_NOT_ENABLED_ERROR"); - super.sendJsonResponse(200, result, resp); } catch (InvalidTotpException e) { result.addProperty("status", "INVALID_TOTP_ERROR"); + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + result.addProperty("currentNumberOfFailedAttempts", e.currentAttempts); + result.addProperty("maxNumberOfFailedAttempts", e.maxAttempts); + } + super.sendJsonResponse(200, result, resp); + } catch (UnknownTotpUserIdException e) { + result.addProperty("status", "UNKNOWN_USER_ID_ERROR"); super.sendJsonResponse(200, result, resp); } catch (LimitReachedException e) { result.addProperty("status", "LIMIT_REACHED_ERROR"); result.addProperty("retryAfterMs", e.retryAfterMs); + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + result.addProperty("currentNumberOfFailedAttempts", e.currentAttempts); + result.addProperty("maxNumberOfFailedAttempts", e.maxAttempts); + } super.sendJsonResponse(200, result, resp); } catch (StorageQueryException | StorageTransactionLogicException | FeatureNotEnabledException | TenantOrAppNotFoundException e) { diff --git a/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpDeviceAPI.java b/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpDeviceAPI.java index a6992c4e6..770ee7bd9 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpDeviceAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpDeviceAPI.java @@ -11,11 +11,12 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; import io.supertokens.totp.Totp; import io.supertokens.totp.exceptions.InvalidTotpException; import io.supertokens.totp.exceptions.LimitReachedException; +import io.supertokens.useridmapping.UserIdType; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -63,18 +64,24 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I result.addProperty("status", "OK"); result.addProperty("wasAlreadyVerified", !isNewlyVerified); super.sendJsonResponse(200, result, resp); - } catch (TotpNotEnabledException e) { - result.addProperty("status", "TOTP_NOT_ENABLED_ERROR"); - super.sendJsonResponse(200, result, resp); } catch (UnknownDeviceException e) { result.addProperty("status", "UNKNOWN_DEVICE_ERROR"); super.sendJsonResponse(200, result, resp); } catch (InvalidTotpException e) { result.addProperty("status", "INVALID_TOTP_ERROR"); + + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + result.addProperty("currentNumberOfFailedAttempts", e.currentAttempts); + result.addProperty("maxNumberOfFailedAttempts", e.maxAttempts); + } super.sendJsonResponse(200, result, resp); } catch (LimitReachedException e) { result.addProperty("status", "LIMIT_REACHED_ERROR"); result.addProperty("retryAfterMs", e.retryAfterMs); + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + result.addProperty("currentNumberOfFailedAttempts", e.currentAttempts); + result.addProperty("maxNumberOfFailedAttempts", e.maxAttempts); + } super.sendJsonResponse(200, result, resp); } catch (StorageQueryException | StorageTransactionLogicException | TenantOrAppNotFoundException e) { throw new ServletException(e); diff --git a/src/test/java/io/supertokens/test/CDIVersionTest.java b/src/test/java/io/supertokens/test/CDIVersionTest.java index 5c2b31e17..6387c1522 100644 --- a/src/test/java/io/supertokens/test/CDIVersionTest.java +++ b/src/test/java/io/supertokens/test/CDIVersionTest.java @@ -26,7 +26,6 @@ import io.supertokens.multitenancy.Multitenancy; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.multitenancy.*; -import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.httpRequest.HttpRequestForTesting; import io.supertokens.test.httpRequest.HttpResponseException; @@ -43,7 +42,6 @@ import org.junit.rules.TestRule; import java.io.IOException; -import java.rmi.ServerException; import java.util.HashMap; import static junit.framework.TestCase.assertEquals; @@ -274,14 +272,14 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, "a1", "t1"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); String response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", diff --git a/src/test/java/io/supertokens/test/CronjobTest.java b/src/test/java/io/supertokens/test/CronjobTest.java index 8baebba6f..7a3d1330f 100644 --- a/src/test/java/io/supertokens/test/CronjobTest.java +++ b/src/test/java/io/supertokens/test/CronjobTest.java @@ -462,6 +462,7 @@ public void testAddingTenantsDoesNotIncreaseCronJobs() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -469,6 +470,7 @@ public void testAddingTenantsDoesNotIncreaseCronJobs() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -476,6 +478,7 @@ public void testAddingTenantsDoesNotIncreaseCronJobs() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -483,6 +486,7 @@ public void testAddingTenantsDoesNotIncreaseCronJobs() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -496,6 +500,7 @@ public void testAddingTenantsDoesNotIncreaseCronJobs() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ), false); @@ -504,6 +509,7 @@ public void testAddingTenantsDoesNotIncreaseCronJobs() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -511,6 +517,7 @@ public void testAddingTenantsDoesNotIncreaseCronJobs() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -518,6 +525,7 @@ public void testAddingTenantsDoesNotIncreaseCronJobs() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ), false); @@ -546,6 +554,7 @@ public void testTargetTenantCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -553,6 +562,7 @@ public void testTargetTenantCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -560,6 +570,7 @@ public void testTargetTenantCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -567,6 +578,7 @@ public void testTargetTenantCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -598,6 +610,7 @@ public void testPerTenantCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -605,6 +618,7 @@ public void testPerTenantCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -612,6 +626,7 @@ public void testPerTenantCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -619,6 +634,7 @@ public void testPerTenantCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -651,6 +667,7 @@ public void testPerAppCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -658,6 +675,7 @@ public void testPerAppCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -665,6 +683,7 @@ public void testPerAppCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -672,6 +691,7 @@ public void testPerAppCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -708,6 +728,7 @@ public void testPerUserPoolCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -715,6 +736,7 @@ public void testPerUserPoolCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); JsonObject config = new JsonObject(); @@ -725,6 +747,7 @@ public void testPerUserPoolCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -732,6 +755,7 @@ public void testPerUserPoolCronTask() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ), false); @@ -771,6 +795,7 @@ public void testThatCoreAutomaticallySyncsToConfigChangesInDb() throws Exception new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), + null, null, new JsonObject() ), false); @@ -791,6 +816,7 @@ public void testThatCoreAutomaticallySyncsToConfigChangesInDb() throws Exception new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), + null, null, new JsonObject() )); @@ -891,6 +917,7 @@ public void testThatCronJobsHaveTenantsInfoAfterRestart() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ), false, false, true); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -898,6 +925,7 @@ public void testThatCronJobsHaveTenantsInfoAfterRestart() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ), false, false, true); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -905,6 +933,7 @@ public void testThatCronJobsHaveTenantsInfoAfterRestart() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ), false, false, true); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -912,6 +941,7 @@ public void testThatCronJobsHaveTenantsInfoAfterRestart() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ), false, false, true); diff --git a/src/test/java/io/supertokens/test/FeatureFlagTest.java b/src/test/java/io/supertokens/test/FeatureFlagTest.java index c842475d9..47abe83aa 100644 --- a/src/test/java/io/supertokens/test/FeatureFlagTest.java +++ b/src/test/java/io/supertokens/test/FeatureFlagTest.java @@ -21,6 +21,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlag; @@ -28,6 +29,7 @@ import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.featureflag.exceptions.NoLicenseKeyFoundException; import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -154,11 +156,11 @@ public void testThatCallingGetFeatureFlagAPIReturnsEmptyArray() throws Exception Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } - private final String OPAQUE_KEY_WITH_TOTP_FEATURE = "pXhNK=nYiEsb6gJEOYP2kIR6M0kn4XLvNqcwT1XbX8xHtm44K" + - "-lQfGCbaeN0Ieeza39fxkXr=tiiUU=DXxDH40Y=4FLT4CE-rG1ETjkXxO4yucLpJvw3uSegPayoISGL"; + private final String OPAQUE_KEY_WITH_MFA_MULTITENANCY_FEATURE = "wtdfQK80jaEYmM1cqlW=lELizFWJlaHOggzvF59jOAwX7NFx" + + "dxH1fw0=RTy=BZixibzF5rn85SNKwfFfLcMm6Li3l1DYOVVD3H8XymCcekti217BxXb-Q6y5r-SKwMOG"; @Test - public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception { + public void testThatCallingGetFeatureFlagAPIReturnsMfaStats() throws Exception { String[] args = {"../"}; TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); @@ -168,7 +170,7 @@ public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception return; } - FeatureFlag.getInstance(process.main).setLicenseKeyAndSyncFeatures(OPAQUE_KEY_WITH_TOTP_FEATURE); + FeatureFlag.getInstance(process.main).setLicenseKeyAndSyncFeatures(OPAQUE_KEY_WITH_MFA_MULTITENANCY_FEATURE); // Get the stats without any users/activity { @@ -184,22 +186,22 @@ public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception if (StorageLayer.isInMemDb(process.main)) { assert features.size() == EE_FEATURES.values().length; } else { - assert features.size() == 1; + assert features.size() == 2; // MFA + MULTITENANCY } - assert features.contains(new JsonPrimitive("totp")); + assert features.contains(new JsonPrimitive("mfa")); assert maus.size() == 31; assert maus.get(0).getAsInt() == 0; assert maus.get(29).getAsInt() == 0; - JsonObject totpStats = usageStats.get("totp").getAsJsonObject(); - JsonArray totpMaus = totpStats.get("maus").getAsJsonArray(); - int totalTotpUsers = totpStats.get("total_users").getAsInt(); + JsonObject mfaStats = usageStats.get("mfa").getAsJsonObject(); + int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); + JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); - assert totpMaus.size() == 31; - assert totpMaus.get(0).getAsInt() == 0; - assert totpMaus.get(29).getAsInt() == 0; + assert mfaMaus.size() == 31; + assert mfaMaus.get(0).getAsInt() == 0; + assert mfaMaus.get(29).getAsInt() == 0; - assert totalTotpUsers == 0; + assert totalMfaUsers == 0; } // First register 2 users for emailpassword recipe. @@ -210,23 +212,25 @@ public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception JsonObject signUpResponse2 = Utils.signUpRequest_2_5(process, "random2@gmail.com", "validPass123"); assert signUpResponse2.get("status").getAsString().equals("OK"); - // Now enable TOTP for the first user by registering a device. - JsonObject body = new JsonObject(); - body.addProperty("userId", signUpResponse.get("user").getAsJsonObject().get("id").getAsString()); - body.addProperty("deviceName", "d1"); - body.addProperty("skew", 0); - body.addProperty("period", 30); - JsonObject res = HttpRequestForTesting.sendJsonPOSTRequest( - process.getProcess(), - "", - "http://localhost:3567/recipe/totp/device", - body, - 1000, - 1000, - null, - Utils.getCdiVersionStringLatestForTests(), - "totp"); - assert res.get("status").getAsString().equals("OK"); + { + // Now enable TOTP for the first user by registering a device. + JsonObject body = new JsonObject(); + body.addProperty("userId", signUpResponse.get("user").getAsJsonObject().get("id").getAsString()); + body.addProperty("deviceName", "d1"); + body.addProperty("skew", 0); + body.addProperty("period", 30); + JsonObject res = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res.get("status").getAsString().equals("OK"); + } // Now check the stats again: { @@ -242,29 +246,155 @@ public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception if (StorageLayer.isInMemDb(process.main)) { assert features.size() == EE_FEATURES.values().length; } else { - assert features.size() == 1; + assert features.size() == 2; // MFA + MULTITENANCY } - assert features.contains(new JsonPrimitive("totp")); + assert features.contains(new JsonPrimitive("mfa")); assert maus.size() == 31; assert maus.get(0).getAsInt() == 2; // 2 users have signed up assert maus.get(29).getAsInt() == 2; - JsonObject totpStats = usageStats.get("totp").getAsJsonObject(); - JsonArray totpMaus = totpStats.get("maus").getAsJsonArray(); - int totalTotpUsers = totpStats.get("total_users").getAsInt(); + JsonObject mfaStats = usageStats.get("mfa").getAsJsonObject(); + int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); + JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); + + assert mfaMaus.size() == 31; + assert mfaMaus.get(0).getAsInt() == 1; // only 1 user has TOTP enabled + assert mfaMaus.get(29).getAsInt() == 1; + + assert totalMfaUsers == 1; + } + + { + // Test with account linking + JsonObject user1 = Utils.signUpRequest_2_5(process, "test1@gmail.com", "validPass123"); + assert signUpResponse.get("status").getAsString().equals("OK"); + + JsonObject user2 = Utils.signUpRequest_2_5(process, "test2@gmail.com", "validPass123"); + assert signUpResponse2.get("status").getAsString().equals("OK"); + + AuthRecipe.createPrimaryUser(process.getProcess(), user1.get("user").getAsJsonObject().get("id").getAsString()); + AuthRecipe.linkAccounts(process.getProcess(), user2.get("user").getAsJsonObject().get("id").getAsString(), user1.get("user").getAsJsonObject().get("id").getAsString()); + + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/ee/featureflag", + null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion().get(), ""); + Assert.assertEquals("OK", response.get("status").getAsString()); + + JsonArray features = response.get("features").getAsJsonArray(); + JsonObject usageStats = response.get("usageStats").getAsJsonObject(); + JsonArray maus = usageStats.get("maus").getAsJsonArray(); + + if (StorageLayer.isInMemDb(process.main)) { + assert features.size() == EE_FEATURES.values().length; + } else { + assert features.size() == 2; // MFA + MULTITENANCY + } + + assert features.contains(new JsonPrimitive("mfa")); + assert maus.size() == 31; + assert maus.get(0).getAsInt() == 4; // 2 users have signed up + assert maus.get(29).getAsInt() == 4; + + { + JsonObject mfaStats = usageStats.get("mfa").getAsJsonObject(); + int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); + JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); + + assert mfaMaus.size() == 31; + assert mfaMaus.get(0).getAsInt() == 2; // 1 TOTP user + 1 account linked user + assert mfaMaus.get(29).getAsInt() == 2; + + assert totalMfaUsers == 2; + } + + // Add TOTP to the linked user + { + JsonObject body = new JsonObject(); + body.addProperty("userId", user1.get("user").getAsJsonObject().get("id").getAsString()); + body.addProperty("deviceName", "d1"); + body.addProperty("skew", 0); + body.addProperty("period", 30); + JsonObject res = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res.get("status").getAsString().equals("OK"); + } + } + + { + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/ee/featureflag", + null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion().get(), ""); + Assert.assertEquals("OK", response.get("status").getAsString()); - assert totpMaus.size() == 31; - assert totpMaus.get(0).getAsInt() == 1; // only 1 user has TOTP enabled - assert totpMaus.get(29).getAsInt() == 1; + JsonArray features = response.get("features").getAsJsonArray(); + JsonObject usageStats = response.get("usageStats").getAsJsonObject(); + JsonArray maus = usageStats.get("maus").getAsJsonArray(); + + { // MFA stats should still count 2 users + JsonObject mfaStats = usageStats.get("mfa").getAsJsonObject(); + int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); + JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); + + assert mfaMaus.size() == 31; + assert mfaMaus.get(0).getAsInt() == 2; // 1 TOTP user + 1 account linked user + assert mfaMaus.get(29).getAsInt() == 2; - assert totalTotpUsers == 1; + assert totalMfaUsers == 2; + } + } + + { // Associate the user with multiple tenants and still the stats should be same + Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( + new TenantIdentifier(null, null, "t1"), + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + null, null, + new JsonObject() + ), false); + Multitenancy.addUserIdToTenant( + process.getProcess(), + new TenantIdentifier(null, null, "t1"), (StorageLayer.getStorage(process.getProcess())), + signUpResponse.get("user").getAsJsonObject().get("id").getAsString() + ); + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/ee/featureflag", + null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion().get(), ""); + Assert.assertEquals("OK", response.get("status").getAsString()); + + JsonArray features = response.get("features").getAsJsonArray(); + JsonObject usageStats = response.get("usageStats").getAsJsonObject(); + JsonArray maus = usageStats.get("maus").getAsJsonArray(); + + { // MFA stats should still count 2 users + JsonObject mfaStats = usageStats.get("mfa").getAsJsonObject(); + int totalMfaUsers = mfaStats.get("totalUserCountWithMoreThanOneLoginMethodOrTOTPEnabled").getAsInt(); + JsonArray mfaMaus = mfaStats.get("mauWithMoreThanOneLoginMethodOrTOTPEnabled").getAsJsonArray(); + + assert mfaMaus.size() == 31; + assert mfaMaus.get(0).getAsInt() == 2; // 1 TOTP user + 1 account linked user + assert mfaMaus.get(29).getAsInt() == 2; + + assert totalMfaUsers == 2; + } } process.kill(); Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + private final static String OPAQUE_KEY_WITH_MFA_FEATURE = "Qk8olVa=v-9PU=snnUFMF4ihMCx4zVBOO6Jd7Nrg6Cg5YyFliEj252ADgpwEpDLfFowA0U5OyVo3XL=U4FMft2HDHCDGg9hWD4iwQQiyjMRi6Mu03CVbAxIkNGaXtJ53"; + + private final String OPAQUE_KEY_WITH_MULTITENANCY_FEATURE = "ijaleljUd2kU9XXWLiqFYv5br8nutTxbyBqWypQdv2N-" + "BocoNriPrnYQd0NXPm8rVkeEocN9ayq0B7c3Pv-BTBIhAZSclXMlgyfXtlwAOJk=9BfESEleW6LyTov47dXu"; @@ -292,6 +422,7 @@ public void testFeatureFlagWithMultitenancyFor500Tenants() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ) ); @@ -353,6 +484,7 @@ public void testThatMultitenantStatsAreAccurate() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -382,6 +514,7 @@ public void testThatMultitenantStatsAreAccurate() throws Exception { null, null, null, null, null, null, null) }), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -454,6 +587,7 @@ public void testThatMultitenantStatsAreAccurateForAnApp() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ) ); @@ -472,6 +606,7 @@ public void testThatMultitenantStatsAreAccurateForAnApp() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -501,6 +636,7 @@ public void testThatMultitenantStatsAreAccurateForAnApp() throws Exception { null, null, null, null, null, null, null) }), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -582,6 +718,7 @@ public void testThatMultitenantStatsAreAccurateForACud() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -601,6 +738,7 @@ public void testThatMultitenantStatsAreAccurateForACud() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -630,6 +768,7 @@ public void testThatMultitenantStatsAreAccurateForACud() throws Exception { null, null, null, null, null, null, null) }), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -702,6 +841,7 @@ public void testPaidFeaturesAreEnabledIfUsingInMemoryDatabase() throws Exception new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ) ); @@ -755,6 +895,9 @@ public void testNetworkCallIsMadeInCoreInit() throws Exception { private final String OPAQUE_KEY_WITH_ACCOUNT_LINKING_FEATURE = "N2uEOdEzd1XZZ5VBSTGYaM7Ia4s8wAqRWFAxLqTYrB6GQ=" + "vssOLo3c=PkFgcExkaXs=IA-d9UWccoNKsyUgNhOhcKtM1bjC5OLrYRpTAgN-2EbKYsQGGQRQHuUN4EO1V"; + private final String OPAQUE_KEY_WTIH_MFA_FEATURE = "F1a=1VUxo7-tHNqFDwuhkkCPCB378A57uRU4=rVW01XBv63YizRb6ItTBu" + + "FHXQIvmceLTlOekCmHv7mwzEZJJKmO9N8pclQSbs4UBz8pzW5d107TIctJgBwy4upnBHUf"; + @Test public void testPaidStatsContainsAllEnabledFeatures() throws Exception { String[] args = {"../"}; @@ -765,7 +908,7 @@ public void testPaidStatsContainsAllEnabledFeatures() throws Exception { String[] licenses = new String[]{ OPAQUE_KEY_WITH_MULTITENANCY_FEATURE, - OPAQUE_KEY_WITH_TOTP_FEATURE, + OPAQUE_KEY_WITH_MFA_FEATURE, OPAQUE_KEY_WITH_DASHBOARD_FEATURE, OPAQUE_KEY_WITH_ACCOUNT_LINKING_FEATURE }; diff --git a/src/test/java/io/supertokens/test/HelloAPITest.java b/src/test/java/io/supertokens/test/HelloAPITest.java index 61049ff3e..f11f9d1b2 100644 --- a/src/test/java/io/supertokens/test/HelloAPITest.java +++ b/src/test/java/io/supertokens/test/HelloAPITest.java @@ -118,6 +118,7 @@ public void testHelloAPIWithBasePath3() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -126,6 +127,7 @@ public void testHelloAPIWithBasePath3() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -134,6 +136,7 @@ public void testHelloAPIWithBasePath3() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -201,6 +204,7 @@ public void testWithBasePathThatHelloAPIDoesNotRequireAPIKeys() throws Exception new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -209,6 +213,7 @@ public void testWithBasePathThatHelloAPIDoesNotRequireAPIKeys() throws Exception new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -217,6 +222,7 @@ public void testWithBasePathThatHelloAPIDoesNotRequireAPIKeys() throws Exception new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -285,6 +291,7 @@ public void testThatHelloAPIDoesNotRequireAPIKeys() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -293,6 +300,7 @@ public void testThatHelloAPIDoesNotRequireAPIKeys() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -301,6 +309,7 @@ public void testThatHelloAPIDoesNotRequireAPIKeys() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); diff --git a/src/test/java/io/supertokens/test/IpAllowDenyRegexTest.java b/src/test/java/io/supertokens/test/IpAllowDenyRegexTest.java index 962997eea..69020cf5a 100644 --- a/src/test/java/io/supertokens/test/IpAllowDenyRegexTest.java +++ b/src/test/java/io/supertokens/test/IpAllowDenyRegexTest.java @@ -385,11 +385,13 @@ public void CheckThatIPFiltersAreTenantSpecific() throws Exception { Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); @@ -425,11 +427,13 @@ public void CheckThatIPFiltersAreTenantSpecific() throws Exception { Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); diff --git a/src/test/java/io/supertokens/test/PathRouterTest.java b/src/test/java/io/supertokens/test/PathRouterTest.java index 9e254c547..3f5879adb 100644 --- a/src/test/java/io/supertokens/test/PathRouterTest.java +++ b/src/test/java/io/supertokens/test/PathRouterTest.java @@ -89,7 +89,7 @@ public void basicTenantIdFetchingTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -100,7 +100,7 @@ public void basicTenantIdFetchingTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -111,7 +111,7 @@ public void basicTenantIdFetchingTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -265,7 +265,7 @@ public void basicTenantIdFetchingWihQueryParamTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -276,7 +276,7 @@ public void basicTenantIdFetchingWihQueryParamTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -287,7 +287,7 @@ public void basicTenantIdFetchingWihQueryParamTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -441,7 +441,7 @@ public void basicTenantIdFetchingWithBasePathTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -452,7 +452,7 @@ public void basicTenantIdFetchingWithBasePathTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -463,7 +463,7 @@ public void basicTenantIdFetchingWithBasePathTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -627,7 +627,7 @@ public void basicTenantIdFetchingWithBasePathTest2() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -638,7 +638,7 @@ public void basicTenantIdFetchingWithBasePathTest2() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -649,7 +649,7 @@ public void basicTenantIdFetchingWithBasePathTest2() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -814,7 +814,7 @@ public void basicTenantIdFetchingWithBasePathTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -825,7 +825,7 @@ public void basicTenantIdFetchingWithBasePathTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -836,7 +836,7 @@ public void basicTenantIdFetchingWithBasePathTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -998,7 +998,7 @@ public void withRecipeRouterTest() throws Exception { new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant( @@ -1008,7 +1008,7 @@ public void withRecipeRouterTest() throws Exception { new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false); @@ -1376,7 +1376,7 @@ public void tenantNotFoundTest() new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -1384,7 +1384,7 @@ public void tenantNotFoundTest() new TenantConfig(new TenantIdentifier("localhost", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -1392,7 +1392,7 @@ public void tenantNotFoundTest() new TenantConfig(new TenantIdentifier("127.0.0.1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenant2Config), + null, null, tenant2Config), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -1400,7 +1400,7 @@ public void tenantNotFoundTest() new TenantConfig(new TenantIdentifier("127.0.0.1", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenant2Config), + null, null, tenant2Config), false ); @@ -1501,7 +1501,7 @@ public void tenantNotFoundTest2() new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -1509,7 +1509,7 @@ public void tenantNotFoundTest2() new TenantConfig(new TenantIdentifier("localhost", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -1517,7 +1517,7 @@ public void tenantNotFoundTest2() new TenantConfig(new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new JsonObject()), + null, null, new JsonObject()), false ); @@ -1627,7 +1627,7 @@ public void tenantNotFoundTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -1637,7 +1637,7 @@ public void tenantNotFoundTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); @@ -1702,7 +1702,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1713,7 +1713,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1724,7 +1724,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1735,7 +1735,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1746,7 +1746,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1757,7 +1757,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1768,7 +1768,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2007,7 +2007,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2018,7 +2018,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2029,7 +2029,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2040,7 +2040,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2051,7 +2051,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2062,7 +2062,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2073,7 +2073,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2296,7 +2296,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2307,7 +2307,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2318,7 +2318,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2329,7 +2329,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2340,7 +2340,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2351,7 +2351,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2362,7 +2362,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2602,7 +2602,7 @@ public void tenantNotFoundWithAppIdTest() new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -2610,7 +2610,7 @@ public void tenantNotFoundWithAppIdTest() new TenantConfig(new TenantIdentifier("localhost", "app1", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -2618,7 +2618,7 @@ public void tenantNotFoundWithAppIdTest() new TenantConfig(new TenantIdentifier("localhost", "app1", "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -2626,7 +2626,7 @@ public void tenantNotFoundWithAppIdTest() new TenantConfig(new TenantIdentifier("127.0.0.1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenant2Config), + null, null, tenant2Config), false ); Webserver.getInstance(process.getProcess()).addAPI(new WebserverAPI(process.getProcess(), "") { @@ -2680,7 +2680,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I new TenantConfig(new TenantIdentifier("127.0.0.1", "app1", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenant2Config), + null, null, tenant2Config), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -2688,7 +2688,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I new TenantConfig(new TenantIdentifier("127.0.0.1", "app1", "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenant2Config), + null, null, tenant2Config), false ); @@ -2746,7 +2746,7 @@ public void tenantNotFoundWithAppIdTest2() new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -2754,7 +2754,7 @@ public void tenantNotFoundWithAppIdTest2() new TenantConfig(new TenantIdentifier("localhost", "app1", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -2762,7 +2762,7 @@ public void tenantNotFoundWithAppIdTest2() new TenantConfig(new TenantIdentifier("localhost", "app1", "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -2770,7 +2770,7 @@ public void tenantNotFoundWithAppIdTest2() new TenantConfig(new TenantIdentifier(null, "app2", null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new JsonObject()), + null, null, new JsonObject()), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -2778,7 +2778,7 @@ public void tenantNotFoundWithAppIdTest2() new TenantConfig(new TenantIdentifier(null, "app2", "t2"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new JsonObject()), + null, null, new JsonObject()), false ); @@ -2919,7 +2919,7 @@ public void tenantNotFoundWithAppIdTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -2929,7 +2929,7 @@ public void tenantNotFoundWithAppIdTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); diff --git a/src/test/java/io/supertokens/test/RequestStatsTest.java b/src/test/java/io/supertokens/test/RequestStatsTest.java index 807ef6e68..553dbc4bd 100644 --- a/src/test/java/io/supertokens/test/RequestStatsTest.java +++ b/src/test/java/io/supertokens/test/RequestStatsTest.java @@ -146,6 +146,7 @@ public void testLastMinuteStatsPerApp() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ), false); diff --git a/src/test/java/io/supertokens/test/StorageLayerTest.java b/src/test/java/io/supertokens/test/StorageLayerTest.java index 7c64fffd5..d649cc7fb 100644 --- a/src/test/java/io/supertokens/test/StorageLayerTest.java +++ b/src/test/java/io/supertokens/test/StorageLayerTest.java @@ -11,7 +11,9 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.totp.TOTPDevice; import io.supertokens.pluginInterface.totp.TOTPUsedCode; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; +import io.supertokens.pluginInterface.totp.exception.DeviceAlreadyExistsException; +import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; +import io.supertokens.pluginInterface.totp.exception.UnknownTotpUserIdException; import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; import io.supertokens.storageLayer.StorageLayer; @@ -47,7 +49,7 @@ public static void insertUsedCodeUtil(TOTPSQLStorage storage, TOTPUsedCode usedC storage.insertUsedCode_Transaction(con, new TenantIdentifier(null, null, null), usedCode); storage.commitTransaction(con); return null; - } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { + } catch (UnknownTotpUserIdException | UsedCodeAlreadyExistsException e) { throw new StorageTransactionLogicException(e); } catch (TenantOrAppNotFoundException e) { throw new IllegalStateException(e); @@ -55,7 +57,8 @@ public static void insertUsedCodeUtil(TOTPSQLStorage storage, TOTPUsedCode usedC }); } catch (StorageTransactionLogicException e) { Exception actual = e.actualException; - if (actual instanceof TotpNotEnabledException || actual instanceof UsedCodeAlreadyExistsException) { + if (actual instanceof UnknownDeviceException || actual instanceof UsedCodeAlreadyExistsException || + actual instanceof UnknownTotpUserIdException) { throw actual; } else { throw e; @@ -82,7 +85,7 @@ public void totpCodeLengthTest() throws Exception { Start start = (Start) StorageLayer.getStorage(process.getProcess()); - TOTPDevice d1 = new TOTPDevice("user", "d1", "secret", 30, 1, false); + TOTPDevice d1 = new TOTPDevice("user", "d1", "secret", 30, 1, false, System.currentTimeMillis()); storage.createDevice(new AppIdentifier(null, null), d1); // Try code with length > 8 @@ -101,5 +104,4 @@ public void totpCodeLengthTest() throws Exception { TOTPUsedCode code = new TOTPUsedCode("user", "12345678", true, nextDay, now); insertUsedCodeUtil(storage, code); } - } diff --git a/src/test/java/io/supertokens/test/StorageTest.java b/src/test/java/io/supertokens/test/StorageTest.java index e0c519ec4..19065d377 100644 --- a/src/test/java/io/supertokens/test/StorageTest.java +++ b/src/test/java/io/supertokens/test/StorageTest.java @@ -751,6 +751,7 @@ public void storageDeadAndAlive() throws InterruptedException, IOException, Http jsonBody.addProperty("refreshToken", sessionCreated.get("refreshToken").getAsJsonObject().get("token").getAsString()); jsonBody.addProperty("enableAntiCsrf", false); + jsonBody.addProperty("useDynamicSigningKey", true); storage.setStorageLayerEnabled(false); diff --git a/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java b/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java index 1e565cec5..aa31aded6 100644 --- a/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java +++ b/src/test/java/io/supertokens/test/SuperTokensSaaSSecretTest.java @@ -358,7 +358,7 @@ public void gettingTenantShouldNotExposeSuperTokensSaaSSecret() new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - new JsonObject())); + null, null, new JsonObject())); TenantConfig[] tenantConfigs = Multitenancy.getAllTenants(process.main); @@ -400,7 +400,7 @@ public void testThatTenantCannotSetSuperTokensSaasSecret() new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - j)); + null, null, j)); fail(); } catch (InvalidConfigException e) { assertEquals(e.getMessage(), "supertokens_saas_secret can only be set via the core's base config setting"); @@ -463,7 +463,7 @@ public void testThatTenantCannotSetProtectedConfigIfSuperTokensSaaSSecretIsSet() Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - j), true); + null, null, j), true); fail(); } catch (BadPermissionException e) { assertEquals(e.getMessage(), "Not allowed to modify protected configs."); @@ -549,7 +549,7 @@ public void testThatTenantCannotGetProtectedConfigIfSuperTokensSaaSSecretIsSet() new TenantConfig(new TenantIdentifier(null, null, "t" + i), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - j)); + null, null, j)); { JsonObject response = HttpRequestForTesting.sendJsonRequest(process.getProcess(), "", @@ -628,7 +628,7 @@ public void testLogContainsCorrectCud() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); { // clear the logs diff --git a/src/test/java/io/supertokens/test/TestHelloAPIRateLimiting.java b/src/test/java/io/supertokens/test/TestHelloAPIRateLimiting.java index 2a1e7f013..f58fe93fe 100644 --- a/src/test/java/io/supertokens/test/TestHelloAPIRateLimiting.java +++ b/src/test/java/io/supertokens/test/TestHelloAPIRateLimiting.java @@ -78,6 +78,7 @@ private void createApps(TestingProcessManager.TestingProcess process) new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), + null, null, config ) ); @@ -98,6 +99,7 @@ private void createApps(TestingProcessManager.TestingProcess process) new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), + null, null, config ) ); @@ -118,6 +120,7 @@ private void createApps(TestingProcessManager.TestingProcess process) new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), + null, null, config ) ); diff --git a/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java b/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java index 5be161397..4417f756f 100644 --- a/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java @@ -423,7 +423,7 @@ public void makePrimaryUserFailsCauseAnotherAccountWithSameEmailAlreadyAPrimaryU 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())); + null, null, new JsonObject())); Storage storage = (StorageLayer.getStorage(process.main)); AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(new TenantIdentifier(null, null, "t1"), @@ -471,7 +471,7 @@ public void makePrimarySucceedsEvenIfAnotherAccountWithSameEmailButInADifferentT 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())); + null, null, new JsonObject())); Storage storage = (StorageLayer.getStorage(process.main)); AuthRecipeUserInfo emailPasswordUser = EmailPassword.signUp(new TenantIdentifier(null, null, "t1"), diff --git a/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java b/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java index 44bca0c5b..cbdfccd5e 100644 --- a/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java @@ -17,6 +17,7 @@ package io.supertokens.test.accountlinking; import com.google.gson.JsonObject; +import io.supertokens.ActiveUsers; import io.supertokens.ProcessState; import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; @@ -37,13 +38,16 @@ 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.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.assertEquals; import static org.junit.Assert.assertNotNull; @@ -466,7 +470,7 @@ public void linkAccountFailureCauseAccountInfoAssociatedWithAPrimaryUserEvenIfIn 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())); + null, null, new JsonObject())); Storage storage = (StorageLayer.getStorage(process.main)); @@ -521,7 +525,7 @@ public void linkAccountSuccessAcrossTenants() throws Exception { 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())); + null, null, new JsonObject())); Storage storage = (StorageLayer.getStorage(process.main)); @@ -607,4 +611,106 @@ public void linkAccountSuccessWithPasswordlessEmailAndPhoneNumber() throws Excep process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void linkAccountMergesLastActiveTimes() 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); + ActiveUsers.updateLastActive(process.main, user.getSupertokensUserId()); + Thread.sleep(50); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + assert (!user2.isPrimaryUser); + long secondUserTime = System.currentTimeMillis(); + ActiveUsers.updateLastActive(process.main, user2.getSupertokensUserId()); + + assertEquals(ActiveUsers.countUsersActiveSince(process.main, 0), 2); + long createPrimaryTime = System.currentTimeMillis(); + AuthRecipe.createPrimaryUser(process.main, user.getSupertokensUserId()); + Thread.sleep(50); + + assertEquals(ActiveUsers.countUsersActiveSince(process.main, 0), 2); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user2.getSupertokensUserId()); + params.addProperty("primaryUserId", 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("OK", response.get("status").getAsString()); + } + + assertEquals(ActiveUsers.countUsersActiveSince(process.main, 0), 1); + assertEquals(ActiveUsers.countUsersActiveSince(process.main, secondUserTime), 1); + assertEquals(ActiveUsers.countUsersActiveSince(process.main, createPrimaryTime), 1); // 1 since we update user last active while linking + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void linkAccountMergesLastActiveTimes_PrimaryFirst() 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); + ActiveUsers.updateLastActive(process.main, user.getSupertokensUserId()); + Thread.sleep(50); + + AuthRecipeUserInfo user2 = EmailPassword.signUp(process.getProcess(), "test2@example.com", "password"); + assert (!user2.isPrimaryUser); + long secondUserTime = System.currentTimeMillis(); + ActiveUsers.updateLastActive(process.main, user2.getSupertokensUserId()); + + assertEquals(ActiveUsers.countUsersActiveSince(process.main, 0), 2); + long createPrimaryTime = System.currentTimeMillis(); + AuthRecipe.createPrimaryUser(process.main, user2.getSupertokensUserId()); + Thread.sleep(50); + + assertEquals(ActiveUsers.countUsersActiveSince(process.main, 0), 2); + + { + 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()); + } + + assertEquals(ActiveUsers.countUsersActiveSince(process.main, 0), 1); + assertEquals(ActiveUsers.countUsersActiveSince(process.main, secondUserTime), 1); + assertEquals(ActiveUsers.countUsersActiveSince(process.main, createPrimaryTime), 1); // 1 since we update user last active while linking + + 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 index 865008ea3..54241e317 100644 --- a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java @@ -100,7 +100,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -120,7 +120,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -140,7 +140,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -160,7 +160,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } diff --git a/src/test/java/io/supertokens/test/accountlinking/SessionTests.java b/src/test/java/io/supertokens/test/accountlinking/SessionTests.java index c84b8aac1..1d249c7d8 100644 --- a/src/test/java/io/supertokens/test/accountlinking/SessionTests.java +++ b/src/test/java/io/supertokens/test/accountlinking/SessionTests.java @@ -36,6 +36,7 @@ import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.session.Session; +import io.supertokens.session.info.SessionInfo; import io.supertokens.session.info.SessionInformationHolder; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; @@ -90,7 +91,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -110,7 +111,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -130,7 +131,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -150,7 +151,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -617,4 +618,44 @@ public void testRevokeSessionsForUserWithAndWithoutIncludingAllLinkedAccounts() process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testCreateSessionWithUserIdMappedForRecipeUser() 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"); + AuthRecipe.createPrimaryUser(process.getProcess(), user1.getSupertokensUserId()); + AuthRecipe.linkAccounts(process.getProcess(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); + + UserIdMapping.createUserIdMapping(process.getProcess(), user1.getSupertokensUserId(), "extid1", null, false); + UserIdMapping.createUserIdMapping(process.getProcess(), user2.getSupertokensUserId(), "extid2", null, false); + + SessionInformationHolder session1 = Session.createNewSession(process.getProcess(), user1.getSupertokensUserId(), new JsonObject(), new JsonObject()); + SessionInformationHolder session2 = Session.createNewSession(process.getProcess(), user2.getSupertokensUserId(), new JsonObject(), new JsonObject()); + SessionInformationHolder session3 = Session.createNewSession(process.getProcess(), "extid1", new JsonObject(), new JsonObject()); + SessionInformationHolder session4 = Session.createNewSession(process.getProcess(), "extid2", new JsonObject(), new JsonObject()); + + assertEquals("extid1", session1.session.userId); + assertEquals("extid1", session1.session.recipeUserId); + assertEquals("extid1", session2.session.userId); + assertEquals("extid2", session2.session.recipeUserId); + assertEquals("extid1", session3.session.userId); + assertEquals("extid1", session3.session.recipeUserId); + assertEquals("extid1", session4.session.userId); + assertEquals("extid2", session4.session.recipeUserId); + + 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 index 8746b2f86..c1edbf517 100644 --- a/src/test/java/io/supertokens/test/accountlinking/api/CreatePrimaryUserAPITest.java +++ b/src/test/java/io/supertokens/test/accountlinking/api/CreatePrimaryUserAPITest.java @@ -452,7 +452,7 @@ public void createPrimaryUserInTenantWithAnotherStorage() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ) ); diff --git a/src/test/java/io/supertokens/test/authRecipe/MultitenantAPITest.java b/src/test/java/io/supertokens/test/authRecipe/MultitenantAPITest.java index 77b0ff9ea..267a8f640 100644 --- a/src/test/java/io/supertokens/test/authRecipe/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/authRecipe/MultitenantAPITest.java @@ -123,6 +123,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -143,6 +144,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -163,6 +165,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); diff --git a/src/test/java/io/supertokens/test/authRecipe/UserPaginationTest.java b/src/test/java/io/supertokens/test/authRecipe/UserPaginationTest.java index ce47e885d..d826cd43e 100644 --- a/src/test/java/io/supertokens/test/authRecipe/UserPaginationTest.java +++ b/src/test/java/io/supertokens/test/authRecipe/UserPaginationTest.java @@ -121,6 +121,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -141,6 +142,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -161,6 +163,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); diff --git a/src/test/java/io/supertokens/test/dashboard/apis/MultitenantAPITest.java b/src/test/java/io/supertokens/test/dashboard/apis/MultitenantAPITest.java index 4ca94e890..28f761d3c 100644 --- a/src/test/java/io/supertokens/test/dashboard/apis/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/dashboard/apis/MultitenantAPITest.java @@ -110,6 +110,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -130,6 +131,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -150,6 +152,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); diff --git a/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java b/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java index 3e6a81c13..235a9c72f 100644 --- a/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java +++ b/src/test/java/io/supertokens/test/emailpassword/EmailPasswordTest.java @@ -936,6 +936,7 @@ public void updateEmailSucceedsIfEmailUsedByOtherPrimaryUserInDifferentTenantWhi 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), + null, null, new JsonObject())); Storage storage = (StorageLayer.getStorage(process.main)); @@ -973,6 +974,7 @@ public void updateEmailFailsIfEmailUsedByOtherPrimaryUserInDifferentTenant() 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), + null, null, new JsonObject())); Storage storage = (StorageLayer.getStorage(process.main)); diff --git a/src/test/java/io/supertokens/test/emailpassword/MultitenantEmailPasswordTest.java b/src/test/java/io/supertokens/test/emailpassword/MultitenantEmailPasswordTest.java index 9ed499fee..64c96c15c 100644 --- a/src/test/java/io/supertokens/test/emailpassword/MultitenantEmailPasswordTest.java +++ b/src/test/java/io/supertokens/test/emailpassword/MultitenantEmailPasswordTest.java @@ -84,6 +84,7 @@ private void createTenants(TestingProcessManager.TestingProcess process) new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), + null, null, config ) ); @@ -104,6 +105,7 @@ private void createTenants(TestingProcessManager.TestingProcess process) new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), + null, null, config ) ); @@ -124,6 +126,7 @@ private void createTenants(TestingProcessManager.TestingProcess process) new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), + null, null, config ) ); 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 8cb5d5ffe..94de67df9 100644 --- a/src/test/java/io/supertokens/test/emailpassword/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/emailpassword/api/MultitenantAPITest.java @@ -117,6 +117,7 @@ private void createTenants(Boolean includeHashingKey) new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), + null, null, config ) ); @@ -141,6 +142,7 @@ private void createTenants(Boolean includeHashingKey) new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), + null, null, config ) ); @@ -165,6 +167,7 @@ private void createTenants(Boolean includeHashingKey) new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), + null, null, config ) ); diff --git a/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest5_0.java b/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest5_0.java new file mode 100644 index 000000000..d2d9c21f7 --- /dev/null +++ b/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest5_0.java @@ -0,0 +1,167 @@ +/* + * 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 java.util.HashMap; + +import static org.junit.Assert.*; + + +public class SignUpAPITest5_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.v5_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)); + } + + @Test + public void testSignUpWithFakeEmailMarksTheEmailAsVerified() 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", "user1.google@@stfakeemail.supertokens.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.v5_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("user1.google@@stfakeemail.supertokens.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(); + assertTrue(lM.get("verified").getAsBoolean()); // Email must be verified + assertNotNull(lM.get("timeJoined")); + assertNotNull(lM.get("recipeUserId")); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "user1.google@@stfakeemail.supertokens.com"); + assert (lM.entrySet().size() == 6); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), beforeSignIn); + assert (activeUsers == 1); + + // double ensure that the email is verified using email verification + + String userId = jsonUser.get("id").getAsString(); + + HashMap map = new HashMap<>(); + map.put("userId", userId); + map.put("email", "user1.google@@stfakeemail.supertokens.com"); + + JsonObject verifyResponse = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/recipe/user/email/verify", map, 1000, 1000, null, + SemVer.v2_7.get(), "emailverification"); + assertEquals(verifyResponse.entrySet().size(), 2); + assertEquals(verifyResponse.get("status").getAsString(), "OK"); + assertTrue(verifyResponse.get("isVerified").getAsBoolean()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/emailverification/api/MultitenantAPITest.java b/src/test/java/io/supertokens/test/emailverification/api/MultitenantAPITest.java index f917bcd9f..5969995d2 100644 --- a/src/test/java/io/supertokens/test/emailverification/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/emailverification/api/MultitenantAPITest.java @@ -105,6 +105,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -125,6 +126,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -145,6 +147,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); diff --git a/src/test/java/io/supertokens/test/mfa/api/CreatePrimaryUserAPITest.java b/src/test/java/io/supertokens/test/mfa/api/CreatePrimaryUserAPITest.java new file mode 100644 index 000000000..c53bb1e2b --- /dev/null +++ b/src/test/java/io/supertokens/test/mfa/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.mfa.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.MFA, 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.MFA, 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.MFA, 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.MFA, 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.MFA, 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.MFA, 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.MFA, 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.MFA, 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), + null, null, coreConfig + ) + ); + + AuthRecipeUserInfo user = EmailPassword.signUp( + tenantIdentifier, (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(), (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/mfa/api/LinkAccountsAPITest.java b/src/test/java/io/supertokens/test/mfa/api/LinkAccountsAPITest.java new file mode 100644 index 000000000..2743c25fa --- /dev/null +++ b/src/test/java/io/supertokens/test/mfa/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.mfa.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.MFA, 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.MFA, 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.MFA, 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.MFA, 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.MFA, 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.MFA, 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.MFA, 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.MFA, 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.MFA, 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.MFA, 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/multitenant/AppTenantUserTest.java b/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java index c00baf80f..aec8e8dff 100644 --- a/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java +++ b/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java @@ -67,7 +67,7 @@ public void testDeletingAppDeleteNonAuthRecipeData() 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}); + EE_FEATURES.MULTI_TENANCY, EE_FEATURES.MFA}); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -106,7 +106,7 @@ public void testDeletingAppDeleteNonAuthRecipeData() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -114,7 +114,7 @@ public void testDeletingAppDeleteNonAuthRecipeData() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); Storage tStorage = ( @@ -146,7 +146,7 @@ public void testDeletingAppDeleteNonAuthRecipeData() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -154,7 +154,7 @@ public void testDeletingAppDeleteNonAuthRecipeData() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); UserIdMapping.findNonAuthStoragesWhereUserIdIsUsedOrAssertIfUsed(t.toAppIdentifier(), tStorage, @@ -172,7 +172,7 @@ public void testDisassociationOfUserDeletesNonAuthRecipeData() 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}); + EE_FEATURES.MULTI_TENANCY, EE_FEATURES.MFA}); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -203,7 +203,7 @@ public void testDisassociationOfUserDeletesNonAuthRecipeData() throws Exception new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -211,7 +211,7 @@ public void testDisassociationOfUserDeletesNonAuthRecipeData() throws Exception new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); Storage appStorage = ( @@ -259,7 +259,7 @@ public void deletingTenantKeepsTheUserInTheApp() 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}); + EE_FEATURES.MULTI_TENANCY, EE_FEATURES.MFA}); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -276,7 +276,7 @@ public void deletingTenantKeepsTheUserInTheApp() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( @@ -284,7 +284,7 @@ public void deletingTenantKeepsTheUserInTheApp() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); Storage appStorage = ( diff --git a/src/test/java/io/supertokens/test/multitenant/ConfigTest.java b/src/test/java/io/supertokens/test/multitenant/ConfigTest.java index 13cdfe6be..23675314c 100644 --- a/src/test/java/io/supertokens/test/multitenant/ConfigTest.java +++ b/src/test/java/io/supertokens/test/multitenant/ConfigTest.java @@ -157,7 +157,7 @@ public void mergingTenantWithBaseConfigWorks() new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig)}, new ArrayList<>()); + null, null, tenantConfig)}, new ArrayList<>()); Assert.assertEquals(Config.getConfig(process.getProcess()).getRefreshTokenValidity(), (long) 144001 * 60 * 1000); @@ -209,7 +209,7 @@ public void mergingTenantWithBaseConfigWithInvalidConfigThrowsErrorWorks() new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig)}, new ArrayList<>()); + null, null, tenantConfig)}, new ArrayList<>()); fail(); } catch (InvalidConfigException e) { assert (e.getMessage() @@ -245,7 +245,7 @@ public void mergingTenantWithBaseConfigWithConflictingConfigsThrowsError() new TenantConfig(new TenantIdentifier(null, null, "abc"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig)}, new ArrayList<>()); + null, null, tenantConfig)}, new ArrayList<>()); fail(); } catch (InvalidConfigException e) { assert (e.getMessage() @@ -295,7 +295,7 @@ public void mergingDifferentUserPoolTenantWithBaseConfigWithConflictingConfigsSh new TenantConfig(new TenantIdentifier("abc", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig)}, new ArrayList<>()); + null, null, tenantConfig)}, new ArrayList<>()); } @@ -344,7 +344,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[0] = new TenantConfig(new TenantIdentifier("c1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig); + null, null, tenantConfig); } { @@ -355,7 +355,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[1] = new TenantConfig(new TenantIdentifier("c1", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig); + null, null, tenantConfig); } { @@ -364,7 +364,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[2] = new TenantConfig(new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig); + null, null, tenantConfig); } { @@ -373,7 +373,7 @@ public void testDifferentWaysToGetConfigBasedOnConnectionURIAndTenantId() tenants[3] = new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig); + null, null, tenantConfig); } Config.loadAllTenantConfig(process.getProcess(), tenants, new ArrayList<>()); @@ -439,7 +439,7 @@ public void testMappingSameUserPoolToDifferentConnectionURIThrowsError() tenants[0] = new TenantConfig(new TenantIdentifier("c1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig); + null, null, tenantConfig); } { @@ -450,7 +450,7 @@ public void testMappingSameUserPoolToDifferentConnectionURIThrowsError() tenants[1] = new TenantConfig(new TenantIdentifier("c2", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig); + null, null, tenantConfig); } try { @@ -490,7 +490,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ) ); @@ -502,7 +502,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ) ); @@ -514,7 +514,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ) ); @@ -526,7 +526,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ) ); @@ -538,7 +538,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ) ); @@ -550,7 +550,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ) ); @@ -567,7 +567,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); @@ -579,7 +579,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); @@ -591,7 +591,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); @@ -603,7 +603,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); @@ -615,7 +615,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); @@ -627,7 +627,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); @@ -639,7 +639,7 @@ public void testCreationOfTenantsUsingValidSourceTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -685,7 +685,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -702,7 +702,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); @@ -714,7 +714,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -731,7 +731,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); @@ -743,7 +743,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -760,7 +760,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -777,7 +777,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -794,7 +794,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -811,7 +811,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -828,7 +828,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -845,7 +845,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -862,7 +862,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -879,7 +879,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -896,7 +896,7 @@ public void testInvalidCasesOfTenantCreation() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); fail(); @@ -936,7 +936,7 @@ public void testUpdationOfDefaultTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ) ); @@ -974,7 +974,7 @@ public void testThatDifferentTenantsInSameAppCannotHaveDifferentAPIKeys() throws new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(false), - coreConfig + null, null, coreConfig ) ); } @@ -992,7 +992,7 @@ public void testThatDifferentTenantsInSameAppCannotHaveDifferentAPIKeys() throws new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(false), - coreConfig + null, null, coreConfig ) ); fail(); @@ -1010,7 +1010,7 @@ public void testThatDifferentTenantsInSameAppCannotHaveDifferentAPIKeys() throws new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ) ); @@ -1025,7 +1025,7 @@ public void testThatDifferentTenantsInSameAppCannotHaveDifferentAPIKeys() throws new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(false), - coreConfig + null, null, coreConfig ) ); } @@ -1042,7 +1042,7 @@ public void testThatDifferentTenantsInSameAppCannotHaveDifferentAPIKeys() throws new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(false), - coreConfig + null, null, coreConfig ) ); } @@ -1118,7 +1118,7 @@ public void testConfigNormalisation() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfigJson + null, null, coreConfigJson ), false); CoreConfig coreConfig = Config.getConfig(tenantIdentifier, process.getProcess()); @@ -1138,7 +1138,7 @@ public void testConfigNormalisation() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfigJson + null, null, coreConfigJson ), false); CoreConfig coreConfig2 = Config.getConfig(tenantIdentifier, process.getProcess()); @@ -1176,7 +1176,7 @@ public void testTenantConfigIsNormalisedFromCUD1() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfigJson + null, null, coreConfigJson ), false); CoreConfig coreConfig = Config.getConfig(tenantIdentifier, process.getProcess()); @@ -1194,7 +1194,7 @@ public void testTenantConfigIsNormalisedFromCUD1() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfigJson + null, null, coreConfigJson ), false); CoreConfig coreConfig = Config.getConfig(tenantIdentifier, process.getProcess()); @@ -1236,7 +1236,7 @@ public void testTenantConfigIsNormalisedFromCUD2() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfigJson + null, null, coreConfigJson ), false); CoreConfig coreConfig = Config.getConfig(tenantIdentifier, process.getProcess()); @@ -1254,7 +1254,7 @@ public void testTenantConfigIsNormalisedFromCUD2() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfigJson + null, null, coreConfigJson ), false); CoreConfig coreConfig = Config.getConfig(tenantIdentifier, process.getProcess()); @@ -1272,7 +1272,7 @@ public void testTenantConfigIsNormalisedFromCUD2() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfigJson + null, null, coreConfigJson ), false); CoreConfig coreConfig = Config.getConfig(tenantIdentifier, process.getProcess()); @@ -1305,7 +1305,7 @@ public void testInvalidConfigWhileCreatingNewTenant() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); fail(); } catch (InvalidConfigException e) { @@ -1338,7 +1338,7 @@ public void testThatConfigChangesReloadsConfig() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); } @@ -1353,7 +1353,7 @@ public void testThatConfigChangesReloadsConfig() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); assertNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.TENANTS_CHANGED_DURING_REFRESH_FROM_DB)); @@ -1373,7 +1373,7 @@ public void testThatConfigChangesReloadsConfig() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); Config configAfter = Config.getInstance(t1, process.getProcess()); @@ -1408,14 +1408,14 @@ public void testThatConfigChangesInAppReloadsConfigInTenant() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( t1, new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); } @@ -1430,14 +1430,14 @@ public void testThatConfigChangesInAppReloadsConfigInTenant() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false);Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( t1, new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig - ), false); + null, null, coreConfig + ), false); assertNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.TENANTS_CHANGED_DURING_REFRESH_FROM_DB)); @@ -1456,7 +1456,7 @@ public void testThatConfigChangesInAppReloadsConfigInTenant() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); Config configAfter = Config.getInstance(t1, process.getProcess()); @@ -1490,7 +1490,7 @@ public void testThatConfigChangesReloadsStorageLayer() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); } @@ -1504,7 +1504,7 @@ public void testThatConfigChangesReloadsStorageLayer() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); assertNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.TENANTS_CHANGED_DURING_REFRESH_FROM_DB)); @@ -1523,7 +1523,7 @@ public void testThatConfigChangesReloadsStorageLayer() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); Storage storageLayerAfter = StorageLayer.getStorage(t1, process.getProcess()); @@ -1543,7 +1543,7 @@ public void testThatConfigChangesReloadsStorageLayer() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); Storage storageLayerAfter = StorageLayer.getStorage(t1, process.getProcess()); @@ -1577,7 +1577,7 @@ public void testThatConfigChangesReloadsFeatureFlag() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); } @@ -1592,7 +1592,7 @@ public void testThatConfigChangesReloadsFeatureFlag() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); assertNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.TENANTS_CHANGED_DURING_REFRESH_FROM_DB)); @@ -1611,7 +1611,7 @@ public void testThatConfigChangesReloadsFeatureFlag() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); FeatureFlag featureFlagAfter = FeatureFlag.getInstance(process.getProcess(), t1); @@ -1645,7 +1645,7 @@ public void testThatConfigChangesReloadsSigningKeys() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); } @@ -1663,7 +1663,7 @@ public void testThatConfigChangesReloadsSigningKeys() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); assertNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.TENANTS_CHANGED_DURING_REFRESH_FROM_DB)); @@ -1692,7 +1692,7 @@ public void testThatConfigChangesReloadsSigningKeys() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - coreConfig + null, null, coreConfig ), false); AccessTokenSigningKey accessTokenSigningKeyAfter = AccessTokenSigningKey.getInstance(t1, process.getProcess()); @@ -1734,7 +1734,7 @@ public void testLoadAllTenantConfigWithDifferentConfigSavedInTheDb() throws Exce new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ), false); // Now load a new set of configs @@ -1753,28 +1753,28 @@ public void testLoadAllTenantConfigWithDifferentConfigSavedInTheDb() throws Exce new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), - config1 + null, null, config1 ), new TenantConfig( new TenantIdentifier(null, "a2", null), new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), - config2 + null, null, config2 ), new TenantConfig( new TenantIdentifier(null, "a2", "t1"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config3 + null, null, config3 ), new TenantConfig( new TenantIdentifier(null, "a1", null), new EmailPasswordConfig(false), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config4 + null, null, config4 ), }; Config.loadAllTenantConfig(process.getProcess(), tenantConfigs); @@ -1820,7 +1820,7 @@ public void testThatMistypedConfigThrowsError() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - mistypedConfig + null, null, mistypedConfig ), false); fail(); } catch (InvalidConfigException e) { @@ -1873,7 +1873,7 @@ public void testCoreSpecificConfigIsNotAllowedForNewTenants() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ), false); fail(); } catch (InvalidConfigException e) { @@ -1964,7 +1964,7 @@ public void testAllConflictingConfigs() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ), false); fail(); } catch (InvalidConfigException e) { @@ -2027,7 +2027,7 @@ public void testAllConflictingConfigs() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ), false); JsonObject config2 = new JsonObject(); @@ -2048,7 +2048,7 @@ public void testAllConflictingConfigs() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config2 + null, null, config2 ), false); fail(); } catch (InvalidConfigException e) { diff --git a/src/test/java/io/supertokens/test/multitenant/LoadTest.java b/src/test/java/io/supertokens/test/multitenant/LoadTest.java index fd321e9f6..5d4ea1f32 100644 --- a/src/test/java/io/supertokens/test/multitenant/LoadTest.java +++ b/src/test/java/io/supertokens/test/multitenant/LoadTest.java @@ -75,7 +75,7 @@ public void testCreating100TenantsAndCheckOnlyOneInstanceOfStorageLayerIsCreated new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - config); + null, null, config); try { Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantIdentifier(null, null, null), tenants[insideLoop]); diff --git a/src/test/java/io/supertokens/test/multitenant/LogTest.java b/src/test/java/io/supertokens/test/multitenant/LogTest.java index 80425ce51..2b7b4fb98 100644 --- a/src/test/java/io/supertokens/test/multitenant/LogTest.java +++ b/src/test/java/io/supertokens/test/multitenant/LogTest.java @@ -19,8 +19,6 @@ import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.ProcessState; -import io.supertokens.cliOptions.CLIOptions; -import io.supertokens.config.Config; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.multitenancy.Multitenancy; @@ -82,39 +80,39 @@ public void testLogThatEachLineIsUniqueOnStartup() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject()), false); + null, null, new JsonObject()), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, "a1", "t1"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject()), false); + null, null, new JsonObject()), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, "a1", "t2"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject()), false); + null, null, new JsonObject()), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, "a2", null), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject()), false); + null, null, new JsonObject()), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, "a2", "t1"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject()), false); + null, null, new JsonObject()), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, "a2", "t2"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject()), false); + null, null, new JsonObject()), false); assertEquals(7, Multitenancy.getAllTenants(process.getProcess()).length); diff --git a/src/test/java/io/supertokens/test/multitenant/RandomConfigTest.java b/src/test/java/io/supertokens/test/multitenant/RandomConfigTest.java index 678093185..4c8dbdeb6 100644 --- a/src/test/java/io/supertokens/test/multitenant/RandomConfigTest.java +++ b/src/test/java/io/supertokens/test/multitenant/RandomConfigTest.java @@ -72,7 +72,7 @@ public void randomlyTestLoadConfig() FeatureFlagTestContent.getInstance(process.getProcess()) .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); process.startProcess(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED, 1000000)); if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; diff --git a/src/test/java/io/supertokens/test/multitenant/RequestConnectionUriDomainTest.java b/src/test/java/io/supertokens/test/multitenant/RequestConnectionUriDomainTest.java index 324f7865b..16612be7c 100644 --- a/src/test/java/io/supertokens/test/multitenant/RequestConnectionUriDomainTest.java +++ b/src/test/java/io/supertokens/test/multitenant/RequestConnectionUriDomainTest.java @@ -147,11 +147,11 @@ public void basicTestingWithDifferentAPIKey() Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), false); + null, null, tenantConfig), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig(new TenantIdentifier("127.0.0.1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenant2Config), false); + null, null, tenant2Config), false); Webserver.getInstance(process.getProcess()).addAPI(new WebserverAPI(process.getProcess(), "") { @@ -255,7 +255,7 @@ public void basicTestingWithDifferentAPIKeyAndTenantId() new TenantConfig(new TenantIdentifier("localhost", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -263,7 +263,7 @@ public void basicTestingWithDifferentAPIKeyAndTenantId() new TenantConfig(new TenantIdentifier("localhost", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -271,7 +271,7 @@ public void basicTestingWithDifferentAPIKeyAndTenantId() new TenantConfig(new TenantIdentifier("127.0.0.1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenant2Config), + null, null, tenant2Config), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -279,7 +279,7 @@ public void basicTestingWithDifferentAPIKeyAndTenantId() new TenantConfig(new TenantIdentifier("127.0.0.1", null, "t1"), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenant2Config), + null, null, tenant2Config), false ); diff --git a/src/test/java/io/supertokens/test/multitenant/SigningKeysTest.java b/src/test/java/io/supertokens/test/multitenant/SigningKeysTest.java index 545748c6d..e456f1f56 100644 --- a/src/test/java/io/supertokens/test/multitenant/SigningKeysTest.java +++ b/src/test/java/io/supertokens/test/multitenant/SigningKeysTest.java @@ -118,7 +118,7 @@ public void keysAreGeneratedForAllUserPoolIds() new TenantConfig(new TenantIdentifier("c1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig)}; + null, null, tenantConfig)}; for (TenantConfig config : tenants) { Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantIdentifier(null, null, null), @@ -189,11 +189,11 @@ public void signingKeyClassesAreThereForAllTenants() new TenantConfig(new TenantIdentifier("c1", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), new TenantConfig(new TenantIdentifier("c2", null, null), new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig2)}; + null, null, tenantConfig2)}; for (TenantConfig config : tenants) { Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantIdentifier(null, null, null), diff --git a/src/test/java/io/supertokens/test/multitenant/StorageLayerTest.java b/src/test/java/io/supertokens/test/multitenant/StorageLayerTest.java index b3e8cb46f..969d8adaa 100644 --- a/src/test/java/io/supertokens/test/multitenant/StorageLayerTest.java +++ b/src/test/java/io/supertokens/test/multitenant/StorageLayerTest.java @@ -183,7 +183,7 @@ public void testUpdationOfDefaultTenant() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); TenantConfig[] tenantConfigs = mtStorage.getAllTenants(); @@ -288,7 +288,7 @@ public void testUpdationOfDefaultTenantWithNullClientType() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); TenantConfig[] tenantConfigs = mtStorage.getAllTenants(); @@ -391,7 +391,7 @@ public void testForNullsInUpdationOfDefaultTenant() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); TenantConfig[] tenantConfigs = mtStorage.getAllTenants(); @@ -485,7 +485,7 @@ public void testForNullClientsListInUpdationOfDefaultTenant() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); TenantConfig[] tenantConfigs = mtStorage.getAllTenants(); @@ -552,7 +552,7 @@ public void testForNullProvidersListInUpdationOfDefaultTenant() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); TenantConfig[] tenantConfigs = mtStorage.getAllTenants(); @@ -636,7 +636,7 @@ public void testCreateTenantPersistsDataCorrectly() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); TenantConfig[] tenantConfigs = mtStorage.getAllTenants(); @@ -752,7 +752,7 @@ public void testCreationOfDuplicationTenantThrowsDuplicateTenantException() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); try { @@ -790,7 +790,7 @@ public void testCreationOfDuplicationTenantThrowsDuplicateTenantException() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); fail(); } catch (DuplicateTenantException e) { @@ -900,11 +900,11 @@ public void testOverwriteTenantOfNonExistantTenantThrowsTenantOrAppNotFoundExcep ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); fail(); } catch (TenantOrAppNotFoundException e) { - // pass + // pass0-89uuuuuui8j= } process.kill(); @@ -1003,7 +1003,7 @@ public void testCreateTenantWithDuplicateProviderIdThrowsException() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); fail(); } catch (DuplicateThirdPartyIdException e) { @@ -1079,7 +1079,7 @@ public void testCreateDuplicateTenantWithDuplicateProviderIdThrowsDuplicateTenan ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); } catch (DuplicateTenantException e) { fail(); @@ -1148,7 +1148,7 @@ public void testCreateDuplicateTenantWithDuplicateProviderIdThrowsDuplicateTenan ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); fail(); } catch (DuplicateTenantException e) { @@ -1224,7 +1224,7 @@ public void testCreateDuplicateTenantWithDuplicateProviderClientTypeThrowsDuplic ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); } catch (DuplicateTenantException e) { fail(); @@ -1273,7 +1273,7 @@ public void testCreateDuplicateTenantWithDuplicateProviderClientTypeThrowsDuplic ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); fail(); } catch (DuplicateTenantException e) { @@ -1385,7 +1385,7 @@ public void testCreateTenantWithDuplicateClientTypeThrowsException() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); fail(); } catch (DuplicateClientTypeException e) { @@ -1489,7 +1489,7 @@ public void testOverwriteTenantWithDuplicateProviderIdThrowsException() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); fail(); } catch (DuplicateThirdPartyIdException e) { @@ -1601,7 +1601,7 @@ public void testOverwriteTenantWithDuplicateClientTypeThrowsException() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); fail(); } catch (DuplicateClientTypeException e) { @@ -1690,7 +1690,7 @@ public void testOverwriteTenantForRaceConditions() ) }), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() )); break; } catch (Exception e) { @@ -1774,28 +1774,28 @@ public void testThatStoragePointingToSameDbSharesThInstance() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config1 + null, null, config1 ), new TenantConfig( new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config1 + null, null, config1 ), new TenantConfig( new TenantIdentifier(null, "a1", null), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config2 + null, null, config2 ), new TenantConfig( new TenantIdentifier(null, "a1", "t1"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config2 + null, null, config2 ) }); @@ -1852,7 +1852,7 @@ public void testThatStorageIsClosedAfterTenantDeletion() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ), false); Storage storage = StorageLayer.getStorage(new TenantIdentifier(null, null, "t1"), process.getProcess()); @@ -1898,14 +1898,14 @@ public void testThatStorageIsClosedOnlyWhenNoMoreTenantsArePointingToIt() throws new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ), false); Storage storage = StorageLayer.getStorage(new TenantIdentifier(null, null, "t1"), process.getProcess()); @@ -1956,14 +1956,14 @@ public void testStorageDoesNotLoadAgainAfterTenantDeletionWhenRefreshedFromDb() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ), false); Multitenancy.addNewOrUpdateAppOrTenant(process.getProcess(), new TenantConfig( new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ), false); @@ -2036,7 +2036,7 @@ public void testThatOriginalStorageIsNotClosedIfTheStorageForATenantChangesAndTh new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); Storage storage = StorageLayer.getBaseStorage(process.getProcess()); @@ -2051,7 +2051,7 @@ public void testThatOriginalStorageIsNotClosedIfTheStorageForATenantChangesAndTh new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ), false); storage = StorageLayer.getBaseStorage(process.getProcess()); diff --git a/src/test/java/io/supertokens/test/multitenant/TestAppData.java b/src/test/java/io/supertokens/test/multitenant/TestAppData.java index 73f8b1209..4f2b33f89 100644 --- a/src/test/java/io/supertokens/test/multitenant/TestAppData.java +++ b/src/test/java/io/supertokens/test/multitenant/TestAppData.java @@ -96,7 +96,7 @@ 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}); + new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY, EE_FEATURES.MFA}); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -104,7 +104,7 @@ public void testThatDeletingAppDeleteDataFromAllTables() throws Exception { return; } - String[] tablesToIgnore = new String[]{"tenant_thirdparty_provider_clients", "tenant_thirdparty_providers"}; + String[] tablesToIgnore = new String[]{"tenant_thirdparty_provider_clients", "tenant_thirdparty_providers", "tenant_first_factors", "tenant_required_secondary_factors"}; TenantIdentifier app = new TenantIdentifier(null, "a1", null); @@ -113,7 +113,7 @@ public void testThatDeletingAppDeleteDataFromAllTables() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); Storage appStorage = ( @@ -159,8 +159,10 @@ public void testThatDeletingAppDeleteDataFromAllTables() throws Exception { TOTPDevice totpDevice = Totp.registerDevice(app.toAppIdentifier(), appStorage, process.getProcess(), epUser.getSupertokensUserId(), "test", 1, 3); + Totp.verifyDevice(app, appStorage, process.getProcess(), epUser.getSupertokensUserId(), totpDevice.deviceName, + generateTotpCode(process.getProcess(), totpDevice, -1)); Totp.verifyCode(app, appStorage, process.getProcess(), epUser.getSupertokensUserId(), - generateTotpCode(process.getProcess(), totpDevice, 0), true); + generateTotpCode(process.getProcess(), totpDevice, 0)); ActiveUsers.updateLastActive(app.toAppIdentifier(), process.getProcess(), epUser.getSupertokensUserId()); diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestApp.java b/src/test/java/io/supertokens/test/multitenant/api/TestApp.java index 5734e5d52..d771e708d 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestApp.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestApp.java @@ -16,6 +16,7 @@ package io.supertokens.test.multitenant.api; +import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.ProcessState; @@ -37,6 +38,7 @@ import io.supertokens.test.httpRequest.HttpRequestForTesting; import io.supertokens.test.httpRequest.HttpResponseException; import io.supertokens.thirdparty.InvalidProviderConfigException; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.Webserver; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -48,6 +50,7 @@ import org.junit.Test; import java.io.IOException; +import java.util.Set; import static org.junit.Assert.*; @@ -508,11 +511,232 @@ public void testDefaultRecipesEnabledWhileCreatingApp() throws Exception { } @Test - public void testInvalidTypedValueInCoreConfigWhileCreatingApp() throws Exception { + public void testFirstFactorsArray() throws Exception { + JsonObject config = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + JsonObject response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + config); + + assertTrue(response.get("createdNew").getAsBoolean()); + + JsonObject tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("firstFactors")); + + // builtin firstFactor + String[] firstFactors = new String[]{"otp-phone"}; + response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + true, new String[]{"otp-phone"}, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(1, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(firstFactors, new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class)); + + response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + false, null, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(1, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(firstFactors, new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class)); + + // custom factors + firstFactors = new String[]{"biometric"}; + response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + true, firstFactors, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(1, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(firstFactors, new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class)); + + // test both + firstFactors = new String[]{"otp-phone", "emailpassword", "biometric", "custom"}; + response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + true, firstFactors, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(4, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(Set.of(firstFactors), Set.of(new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class))); + + response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + true, null, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("firstFactors")); + } + + @Test + public void testRequiredSecondaryFactorsArray() throws Exception { if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; } + JsonObject config = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + JsonObject response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + config); + + assertTrue(response.get("createdNew").getAsBoolean()); + + JsonObject tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("requiredSecondaryFactors")); + + // builtin firstFactor + String[] requiredSecondaryFactors = new String[]{"otp-phone"}; + response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + false, null, true, new String[]{"otp-phone"}, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(1, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(requiredSecondaryFactors, new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class)); + + response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + false, null, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(1, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(requiredSecondaryFactors, new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class)); + + // custom factors + requiredSecondaryFactors = new String[]{"biometric"}; + response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + false, null, true, requiredSecondaryFactors, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(1, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(requiredSecondaryFactors, new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class)); + + // test both + requiredSecondaryFactors = new String[]{"otp-phone", "emailpassword", "biometric", "custom"}; + response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + false, null, true, requiredSecondaryFactors, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(4, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(Set.of(requiredSecondaryFactors), Set.of(new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class))); + + response = TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + false, null, true, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, "a1", null), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("requiredSecondaryFactors")); + } + + @Test + public void testDuplicateValuesInFirstFactorsAndRequiredSecondaryFactors() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject config = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + String[] factors = new String[]{"duplicate", "emailpassword", "duplicate", "custom"}; + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + true, factors, false, null, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: firstFactors input should not contain duplicate values", e.getMessage()); + } + + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + false, null, true, factors, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: requiredSecondaryFactors input should not contain duplicate values", e.getMessage()); + } + } + + @Test + public void testInvalidTypedValueInCoreConfigWhileCreatingApp() throws Exception { if (StorageLayer.isInMemDb(process.getProcess())) { return; } @@ -584,6 +808,302 @@ else if (values[i] instanceof String) { } @Test + public void testFirstFactorArrayValueValidationBasedOnDisabledRecipe() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject config = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", true, true, true, + false, null, false, null, + config, SemVer.v5_0); + + { + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", false, null, null, + true, new String[]{}, false, null, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: firstFactors cannot be empty. Set null instead to remove all first factors.", + e.getMessage()); + } + } + + { + String[] factors = new String[]{"emailpassword", "custom"}; + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", false, null, null, + true, factors, false, null, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: firstFactors should not contain 'emailpassword' because emailPassword is disabled for the tenant.", e.getMessage()); + } + + { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", false, null, null, + false, null, false, null, + config, SemVer.v5_0); + } + + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + true, factors, false, null, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: firstFactors should not contain 'emailpassword' because emailPassword is disabled for the tenant.", e.getMessage()); + } + } + + { + String[] factors = new String[]{"otp-email", "custom"}; + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, false, + true, factors, false, null, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: firstFactors should not contain 'otp-email' because passwordless is disabled for the tenant.", e.getMessage()); + } + + { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, false, + false, null, false, null, + config, SemVer.v5_0); + } + + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + true, factors, false, null, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: firstFactors should not contain 'otp-email' because passwordless is disabled for the tenant.", e.getMessage()); + } + } + + { + String[] factors = new String[]{"thirdparty", "custom"}; + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, false, null, + true, factors, false, null, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: firstFactors should not contain 'thirdparty' because thirdParty is disabled for the tenant.", e.getMessage()); + } + + { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, false, null, + false, null, false, null, + config, SemVer.v5_0); + } + + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + true, factors, false, null, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: firstFactors should not contain 'thirdparty' because thirdParty is disabled for the tenant.", e.getMessage()); + } + } + + } + + @Test + public void testRequiredSecondaryFactorArrayValueValidationBasedOnDisabledRecipe() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject config = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", true, true, true, + false, null, false, null, + config, SemVer.v5_0); + + { + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", false, null, null, + false, null, true, new String[]{}, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: requiredSecondaryFactors cannot be empty. Set null instead to remove all required secondary factors.", + e.getMessage()); + } + } + + { + String[] factors = new String[]{"emailpassword", "custom"}; + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", false, null, null, + false, null, true, factors, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: requiredSecondaryFactors should not contain 'emailpassword' because emailPassword is disabled for the tenant.", e.getMessage()); + } + + { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", false, null, null, + false, null, false, null, + config, SemVer.v5_0); + } + + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + false, null, true, factors, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: requiredSecondaryFactors should not contain 'emailpassword' because emailPassword is disabled for the tenant.", e.getMessage()); + } + } + + { + String[] factors = new String[]{"otp-email", "custom"}; + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, false, + false, null, true, factors, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: requiredSecondaryFactors should not contain 'otp-email' because passwordless is disabled for the tenant.", e.getMessage()); + } + + { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, false, + false, null, false, null, + config, SemVer.v5_0); + } + + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + false, null, true, factors, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: requiredSecondaryFactors should not contain 'otp-email' because passwordless is disabled for the tenant.", e.getMessage()); + } + } + + { + String[] factors = new String[]{"thirdparty", "custom"}; + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, false, null, + false, null, true, factors, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: requiredSecondaryFactors should not contain 'thirdparty' because thirdParty is disabled for the tenant.", e.getMessage()); + } + + { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, false, null, + false, null, false, null, + config, SemVer.v5_0); + } + + try { + TestMultitenancyAPIHelper.createApp( + process.getProcess(), + new TenantIdentifier(null, null, null), + "a1", null, null, null, + false, null, true, factors, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: Invalid core config: requiredSecondaryFactors should not contain 'thirdparty' because thirdParty is disabled for the tenant.", e.getMessage()); + } + } + } + public void testInvalidCoreConfig() throws Exception { if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestConnectionUriDomain.java b/src/test/java/io/supertokens/test/multitenant/api/TestConnectionUriDomain.java index a560b70a3..31102bb5e 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestConnectionUriDomain.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestConnectionUriDomain.java @@ -16,6 +16,7 @@ package io.supertokens.test.multitenant.api; +import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.supertokens.ProcessState; @@ -35,6 +36,7 @@ import io.supertokens.test.httpRequest.HttpRequestForTesting; import io.supertokens.test.httpRequest.HttpResponseException; import io.supertokens.thirdparty.InvalidProviderConfigException; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.Webserver; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -46,6 +48,7 @@ import org.junit.Test; import java.io.IOException; +import java.util.Set; import static org.junit.Assert.*; @@ -488,4 +491,242 @@ public void testDefaultRecipesEnabledWhileCreatingCUD() throws Exception { assertTrue(tenant.get("thirdParty").getAsJsonObject().get("enabled").getAsBoolean()); assertTrue(tenant.get("passwordless").getAsJsonObject().get("enabled").getAsBoolean()); } + + @Test + public void testFirstFactorsArray() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + JsonObject config = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + JsonObject response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + config); + + assertTrue(response.get("createdNew").getAsBoolean()); + + JsonObject tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("firstFactors")); + + // builtin firstFactor + String[] firstFactors = new String[]{"otp-phone"}; + response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + true, new String[]{"otp-phone"}, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(1, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(firstFactors, new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class)); + + response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + false, null, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(1, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(firstFactors, new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class)); + + // custom factors + firstFactors = new String[]{"biometric"}; + response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + true, firstFactors, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(1, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(firstFactors, new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class)); + + // test both + firstFactors = new String[]{"otp-phone", "emailpassword", "biometric", "custom"}; + response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + true, firstFactors, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(4, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(Set.of(firstFactors), Set.of(new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class))); + + response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + true, null, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("firstFactors")); + } + + @Test + public void testRequiredSecondaryFactorsArray() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + JsonObject config = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + JsonObject response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + config); + + assertTrue(response.get("createdNew").getAsBoolean()); + + JsonObject tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("requiredSecondaryFactors")); + + // builtin firstFactor + String[] requiredSecondaryFactors = new String[]{"otp-phone"}; + response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + false, null, true, new String[]{"otp-phone"}, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(1, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(requiredSecondaryFactors, new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class)); + + response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + false, null, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(1, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(requiredSecondaryFactors, new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class)); + + // custom factors + requiredSecondaryFactors = new String[]{"biometric"}; + response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + false, null, true, requiredSecondaryFactors, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(1, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(requiredSecondaryFactors, new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class)); + + // test both + requiredSecondaryFactors = new String[]{"otp-phone", "emailpassword", "biometric", "custom"}; + response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + false, null, true, requiredSecondaryFactors, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(4, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(Set.of(requiredSecondaryFactors), Set.of(new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class))); + + response = TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + false, null, true, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier("127.0.0.1", null, null), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("requiredSecondaryFactors")); + } + + @Test + public void testDuplicateValuesInFirstFactorsAndRequiredSecondaryFactors() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject config = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + String[] factors = new String[]{"duplicate", "emailpassword", "duplicate", "custom"}; + try { + TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + true, factors, false, null, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: firstFactors input should not contain duplicate values", e.getMessage()); + } + + try { + TestMultitenancyAPIHelper.createConnectionUriDomain( + process.getProcess(), + new TenantIdentifier(null, null, null), + "127.0.0.1", null, null, null, + false, null, true, factors, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: requiredSecondaryFactors input should not contain duplicate values", e.getMessage()); + } + + } } 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 d5268a353..6a836486a 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestMultitenancyAPIHelper.java @@ -17,7 +17,6 @@ package io.supertokens.test.multitenant.api; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import io.supertokens.Main; import io.supertokens.pluginInterface.RECIPE_ID; @@ -39,6 +38,16 @@ public class TestMultitenancyAPIHelper { public static JsonObject createConnectionUriDomain(Main main, TenantIdentifier sourceTenant, String connectionUriDomain, Boolean emailPasswordEnabled, Boolean thirdPartyEnabled, Boolean passwordlessEnabled, JsonObject coreConfig) throws HttpResponseException, IOException { + return createConnectionUriDomain(main, sourceTenant, connectionUriDomain, emailPasswordEnabled, thirdPartyEnabled, + passwordlessEnabled, false, null, false, null, coreConfig, SemVer.v3_0); + + } + + public static JsonObject createConnectionUriDomain(Main main, TenantIdentifier sourceTenant, String connectionUriDomain, Boolean emailPasswordEnabled, + Boolean thirdPartyEnabled, Boolean passwordlessEnabled, + boolean setFirstFactors, String[] firstFactors, + boolean setRequiredSecondaryFactors, String[] requiredSecondaryFactors, + JsonObject coreConfig, SemVer version) throws HttpResponseException, IOException { JsonObject requestBody = new JsonObject(); if (connectionUriDomain != null) { requestBody.addProperty("connectionUriDomain", connectionUriDomain); @@ -52,12 +61,19 @@ public static JsonObject createConnectionUriDomain(Main main, TenantIdentifier s if (passwordlessEnabled != null) { requestBody.addProperty("passwordlessEnabled", passwordlessEnabled); } + if (setFirstFactors || firstFactors != null) { + requestBody.add("firstFactors", new Gson().toJsonTree(firstFactors)); + } + if (setRequiredSecondaryFactors || requiredSecondaryFactors != null) { + requestBody.add("requiredSecondaryFactors", new Gson().toJsonTree(requiredSecondaryFactors)); + } + requestBody.add("coreConfig", coreConfig); JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", HttpRequestForTesting.getMultitenantUrl(sourceTenant, "/recipe/multitenancy/connectionuridomain"), requestBody, 1000, 2500, null, - SemVer.v3_0.get(), "multitenancy"); + version.get(), "multitenancy"); assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); @@ -93,6 +109,15 @@ public static JsonObject deleteConnectionUriDomain(TenantIdentifier sourceTenant public static JsonObject createApp(Main main, TenantIdentifier sourceTenant, String appId, Boolean emailPasswordEnabled, Boolean thirdPartyEnabled, Boolean passwordlessEnabled, JsonObject coreConfig) throws HttpResponseException, IOException { + return createApp(main, sourceTenant, appId, emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled, + false, null, false, null, coreConfig, SemVer.v3_0); + } + + public static JsonObject createApp(Main main, TenantIdentifier sourceTenant, String appId, Boolean emailPasswordEnabled, + Boolean thirdPartyEnabled, Boolean passwordlessEnabled, + boolean setFirstFactors, String[] firstFactors, + boolean setRequiredSecondaryFactors, String[] requiredSecondaryFactors, + JsonObject coreConfig, SemVer version) throws HttpResponseException, IOException { JsonObject requestBody = new JsonObject(); requestBody.addProperty("appId", appId); if (emailPasswordEnabled != null) { @@ -104,12 +129,18 @@ public static JsonObject createApp(Main main, TenantIdentifier sourceTenant, Str if (passwordlessEnabled != null) { requestBody.addProperty("passwordlessEnabled", passwordlessEnabled); } + if (setFirstFactors || firstFactors != null) { + requestBody.add("firstFactors", new Gson().toJsonTree(firstFactors)); + } + if (setRequiredSecondaryFactors || requiredSecondaryFactors != null) { + requestBody.add("requiredSecondaryFactors", new Gson().toJsonTree(requiredSecondaryFactors)); + } requestBody.add("coreConfig", coreConfig); JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", HttpRequestForTesting.getMultitenantUrl(sourceTenant, "/recipe/multitenancy/app"), requestBody, 1000, 2500, null, - SemVer.v3_0.get(), "multitenancy"); + version.get(), "multitenancy"); assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); return response; @@ -143,6 +174,15 @@ public static JsonObject deleteApp(TenantIdentifier sourceTenant, String appId, public static JsonObject createTenant(Main main, TenantIdentifier sourceTenant, String tenantId, Boolean emailPasswordEnabled, Boolean thirdPartyEnabled, Boolean passwordlessEnabled, JsonObject coreConfig) throws HttpResponseException, IOException { + return createTenant(main, sourceTenant, tenantId, emailPasswordEnabled, thirdPartyEnabled, passwordlessEnabled, + false, null, false, null, coreConfig, SemVer.v3_0); + } + + public static JsonObject createTenant(Main main, TenantIdentifier sourceTenant, String tenantId, Boolean emailPasswordEnabled, + Boolean thirdPartyEnabled, Boolean passwordlessEnabled, + boolean setFirstFactors, String[] firstFactors, + boolean setRequiredSecondaryFactors, String[] requiredSecondaryFactors, + JsonObject coreConfig, SemVer version) throws HttpResponseException, IOException { JsonObject requestBody = new JsonObject(); requestBody.addProperty("tenantId", tenantId); if (emailPasswordEnabled != null) { @@ -154,12 +194,19 @@ public static JsonObject createTenant(Main main, TenantIdentifier sourceTenant, if (passwordlessEnabled != null) { requestBody.addProperty("passwordlessEnabled", passwordlessEnabled); } + if (setFirstFactors || firstFactors != null) { + requestBody.add("firstFactors", new Gson().toJsonTree(firstFactors)); + } + if (setRequiredSecondaryFactors || requiredSecondaryFactors != null) { + requestBody.add("requiredSecondaryFactors", new Gson().toJsonTree(requiredSecondaryFactors)); + } + requestBody.add("coreConfig", coreConfig); JsonObject response = HttpRequestForTesting.sendJsonPUTRequest(main, "", HttpRequestForTesting.getMultitenantUrl(sourceTenant, "/recipe/multitenancy/tenant"), requestBody, 1000, 2500, null, - SemVer.v3_0.get(), "multitenancy"); + version.get(), "multitenancy"); assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); return response; @@ -192,11 +239,16 @@ public static JsonObject deleteTenant(TenantIdentifier sourceTenant, String tena public static JsonObject getTenant(TenantIdentifier tenantIdentifier, Main main) throws HttpResponseException, IOException { + return getTenant(tenantIdentifier, main, SemVer.v3_0); + } + + public static JsonObject getTenant(TenantIdentifier tenantIdentifier, Main main, SemVer version) + throws HttpResponseException, IOException { JsonObject response = HttpRequestForTesting.sendGETRequest(main, "", HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/multitenancy/tenant"), null, 1000, 1000, null, - SemVer.v3_0.get(), "multitenancy"); + version.get(), "multitenancy"); assertEquals("OK", response.getAsJsonPrimitive("status").getAsString()); return response; @@ -290,12 +342,33 @@ public static JsonObject epSignUp(TenantIdentifier tenantIdentifier, String emai JsonObject requestBody = new JsonObject(); requestBody.addProperty("email", email); requestBody.addProperty("password", password); + JsonObject signUpResponse = epSignUpAndGetResponse(tenantIdentifier, email, password, main, SemVer.v3_0); + assertEquals("OK", signUpResponse.getAsJsonPrimitive("status").getAsString()); + return signUpResponse.getAsJsonObject("user"); + } + + public static JsonObject epSignUpAndGetResponse(TenantIdentifier tenantIdentifier, String email, String password, Main main, SemVer version) + throws HttpResponseException, IOException { + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("email", email); + requestBody.addProperty("password", password); JsonObject signUpResponse = HttpRequestForTesting.sendJsonPOSTRequest(main, "", HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signup"), requestBody, 1000, 1000, null, - SemVer.v3_0.get(), "emailpassword"); - assertEquals("OK", signUpResponse.getAsJsonPrimitive("status").getAsString()); - return signUpResponse.getAsJsonObject("user"); + version.get(), "emailpassword"); + return signUpResponse; + } + + public static JsonObject epSignInAndGetResponse(TenantIdentifier tenantIdentifier, String email, String password, Main main, SemVer version) + throws HttpResponseException, IOException { + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("email", email); + requestBody.addProperty("password", password); + JsonObject signUpResponse = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signin"), + requestBody, 1000, 1000, null, + version.get(), "emailpassword"); + return signUpResponse; } public static JsonObject tpSignInUp(TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId, String email, Main main) @@ -308,16 +381,31 @@ public static JsonObject tpSignInUp(TenantIdentifier tenantIdentifier, String th signUpRequestBody.addProperty("thirdPartyUserId", thirdPartyUserId); signUpRequestBody.add("email", emailObject); - JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", - HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup"), signUpRequestBody, - 1000, 1000, null, - SemVer.v3_0.get(), "thirdparty"); + JsonObject response = tpSignInUpAndGetResponse(tenantIdentifier, thirdPartyId, thirdPartyUserId, email, main, SemVer.v3_0); assertEquals("OK", response.get("status").getAsString()); assertEquals(3, response.entrySet().size()); return response.get("user").getAsJsonObject(); } + public static JsonObject tpSignInUpAndGetResponse(TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId, String email, Main main, SemVer version) + throws HttpResponseException, IOException { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", email); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", thirdPartyId); + signUpRequestBody.addProperty("thirdPartyUserId", thirdPartyUserId); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup"), signUpRequestBody, + 1000, 1000, null, + version.get(), "thirdparty"); + return response; + } + private static String generateRandomString(int length) { StringBuilder sb = new StringBuilder(length); final String ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; @@ -332,6 +420,11 @@ private static String generateRandomString(int length) { private static JsonObject createCodeWithEmail(TenantIdentifier tenantIdentifier, String email, Main main) throws HttpResponseException, IOException { + return createCodeWithEmail(tenantIdentifier, email, main, SemVer.v3_0); + } + + private static JsonObject createCodeWithEmail(TenantIdentifier tenantIdentifier, String email, Main main, SemVer version) + throws HttpResponseException, IOException { String exampleCode = generateRandomString(6); JsonObject createCodeRequestBody = new JsonObject(); createCodeRequestBody.addProperty("email", email); @@ -340,7 +433,7 @@ private static JsonObject createCodeWithEmail(TenantIdentifier tenantIdentifier, JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(main, "", HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup/code"), createCodeRequestBody, 1000, 1000, null, - SemVer.v3_0.get(), "passwordless"); + version.get(), "passwordless"); assertEquals("OK", response.get("status").getAsString()); assertEquals(8, response.entrySet().size()); @@ -349,36 +442,82 @@ private static JsonObject createCodeWithEmail(TenantIdentifier tenantIdentifier, } private static JsonObject consumeCode(TenantIdentifier tenantIdentifier, String deviceId, String preAuthSessionId, - String userInputCode, Main main) + String userInputCode, Main main) throws HttpResponseException, IOException { + return consumeCode(tenantIdentifier, deviceId, preAuthSessionId, userInputCode, main, SemVer.v3_0); + } + + private static JsonObject consumeCode(TenantIdentifier tenantIdentifier, String deviceId, String preAuthSessionId, + String userInputCode, Main main, SemVer version) 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"); + JsonObject response = consumeCodeAndGetResponse(tenantIdentifier, deviceId, preAuthSessionId, userInputCode, main, version); assertEquals("OK", response.get("status").getAsString()); return response.get("user").getAsJsonObject(); } + private static JsonObject consumeCodeAndGetResponse(TenantIdentifier tenantIdentifier, String deviceId, String preAuthSessionId, + String userInputCode, Main main, SemVer version) + throws HttpResponseException, IOException { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", preAuthSessionId); + consumeCodeRequestBody.addProperty("userInputCode", userInputCode); + + return HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup/code/consume"), + consumeCodeRequestBody, 1000, 1000, null, + version.get(), "passwordless"); + } + + private static JsonObject consumeCodeAndGetResponse(TenantIdentifier tenantIdentifier, String preAuthSessionId, + String linkCode, Main main, SemVer version) + throws HttpResponseException, IOException { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", preAuthSessionId); + consumeCodeRequestBody.addProperty("linkCode", linkCode); + + return HttpRequestForTesting.sendJsonPOSTRequest(main, "", + HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/signinup/code/consume"), + consumeCodeRequestBody, 1000, 1000, null, + version.get(), "passwordless"); + } + 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); } + public static JsonObject plSignInUpWithEmailOTP(TenantIdentifier tenantIdentifier, String email, Main main, SemVer version) + throws HttpResponseException, IOException { + JsonObject code = createCodeWithEmail(tenantIdentifier, email, main, version); + return consumeCodeAndGetResponse(tenantIdentifier, code.get("deviceId").getAsString(), code.get("preAuthSessionId").getAsString(), code.get("userInputCode").getAsString(), main, version); + } + + public static JsonObject plSignInUpWithEmailLink(TenantIdentifier tenantIdentifier, String email, Main main, SemVer version) + throws HttpResponseException, IOException { + JsonObject code = createCodeWithEmail(tenantIdentifier, email, main, version); + return consumeCodeAndGetResponse(tenantIdentifier, code.get("preAuthSessionId").getAsString(), code.get("linkCode").getAsString(), main, version); + } + private static JsonObject createCodeWithNumber(TenantIdentifier tenantIdentifier, String phoneNumber, Main main) throws HttpResponseException, IOException { + return createCodeWithNumber(tenantIdentifier, phoneNumber, main, SemVer.v3_0); + } + + private static JsonObject createCodeWithNumber(TenantIdentifier tenantIdentifier, String phoneNumber, Main main, SemVer version) + 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"); + version.get(), "passwordless"); assertEquals("OK", response.get("status").getAsString()); assertEquals(8, response.entrySet().size()); @@ -392,6 +531,18 @@ public static JsonObject plSignInUpNumber(TenantIdentifier tenantIdentifier, Str return consumeCode(tenantIdentifier, code.get("deviceId").getAsString(), code.get("preAuthSessionId").getAsString(), code.get("userInputCode").getAsString(), main); } + public static JsonObject plSignInUpWithPhoneOTP(TenantIdentifier tenantIdentifier, String phoneNumber, Main main, SemVer version) + throws HttpResponseException, IOException { + JsonObject code = createCodeWithNumber(tenantIdentifier, phoneNumber, main, version); + return consumeCodeAndGetResponse(tenantIdentifier, code.get("deviceId").getAsString(), code.get("preAuthSessionId").getAsString(), code.get("userInputCode").getAsString(), main, version); + } + + public static JsonObject plSignInUpWithPhoneLink(TenantIdentifier tenantIdentifier, String phoneNumber, Main main, SemVer version) + throws HttpResponseException, IOException { + JsonObject code = createCodeWithNumber(tenantIdentifier, phoneNumber, main, version); + return consumeCodeAndGetResponse(tenantIdentifier, code.get("preAuthSessionId").getAsString(), code.get("linkCode").getAsString(), main, version); + } + 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/TestPermissionChecks.java b/src/test/java/io/supertokens/test/multitenant/api/TestPermissionChecks.java index cc50c1938..e2f9e4a32 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestPermissionChecks.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestPermissionChecks.java @@ -218,15 +218,15 @@ public void testPermissionsForListTenants() throws Exception { TestCase[] testCases = new TestCase[]{ new TestCase( new TenantIdentifier("127.0.0.1", "a1", "t1"), null, - "Only the public tenantId is allowed to list all tenants" + "Only the public tenantId is allowed to list all tenants associated with this app" ), new TestCase( new TenantIdentifier("127.0.0.1", null, "t1"), null, - "Only the public tenantId is allowed to list all tenants" + "Only the public tenantId is allowed to list all tenants associated with this app" ), new TestCase( new TenantIdentifier(null, null, "t1"), null, - "Only the public tenantId is allowed to list all tenants" + "Only the public tenantId is allowed to list all tenants associated with this app" ), new TestCase( new TenantIdentifier(null, null, null), null, null diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestSkipValidationInCreateThirdParty.java b/src/test/java/io/supertokens/test/multitenant/api/TestSkipValidationInCreateThirdParty.java index d99ae5658..d4f6472ef 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestSkipValidationInCreateThirdParty.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestSkipValidationInCreateThirdParty.java @@ -68,7 +68,7 @@ public void testSkipValidation() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); try { diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestTenant.java b/src/test/java/io/supertokens/test/multitenant/api/TestTenant.java index 0d222190a..60741ee52 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestTenant.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestTenant.java @@ -16,8 +16,7 @@ package io.supertokens.test.multitenant.api; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.google.gson.*; import io.supertokens.ProcessState; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; @@ -35,6 +34,7 @@ import io.supertokens.test.httpRequest.HttpRequestForTesting; import io.supertokens.test.httpRequest.HttpResponseException; import io.supertokens.thirdparty.InvalidProviderConfigException; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.Webserver; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -46,6 +46,7 @@ import org.junit.Test; import java.io.IOException; +import java.util.Set; import static org.junit.Assert.*; @@ -347,4 +348,234 @@ public void testDefaultRecipesEnabledWhileCreatingTenant() throws Exception { assertFalse(tenant.get("thirdParty").getAsJsonObject().get("enabled").getAsBoolean()); assertFalse(tenant.get("passwordless").getAsJsonObject().get("enabled").getAsBoolean()); } + + @Test + public void testFirstFactorsArray() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject config = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + JsonObject response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, null, + config); + + assertTrue(response.get("createdNew").getAsBoolean()); + + JsonObject tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("firstFactors")); + + // builtin firstFactor + String[] firstFactors = new String[]{"otp-phone"}; + response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, true, + true, new String[]{"otp-phone"}, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(1, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(firstFactors, new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class)); + + response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, null, + false, null, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(1, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(firstFactors, new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class)); + + // custom factors + firstFactors = new String[]{"biometric"}; + response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, null, + true, firstFactors, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(1, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(firstFactors, new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class)); + + // test both + firstFactors = new String[]{"otp-phone", "emailpassword", "biometric", "custom"}; + response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", true, null, true, + true, firstFactors, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("firstFactors").isJsonArray()); + assertEquals(4, tenant.get("firstFactors").getAsJsonArray().size()); + assertEquals(Set.of(firstFactors), Set.of(new Gson().fromJson(tenant.get("firstFactors").getAsJsonArray(), String[].class))); + + response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, null, + true, null, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("firstFactors")); + } + + @Test + public void testRequiredSecondaryFactorsArray() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject config = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + JsonObject response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, null, + config); + + assertTrue(response.get("createdNew").getAsBoolean()); + + JsonObject tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("requiredSecondaryFactors")); + + // builtin firstFactor + String[] requiredSecondaryFactors = new String[]{"otp-phone"}; + response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, true, + false, null, true, new String[]{"otp-phone"}, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(1, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(requiredSecondaryFactors, new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class)); + + response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, null, + false, null, false, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(1, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(requiredSecondaryFactors, new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class)); + + // custom factors + requiredSecondaryFactors = new String[]{"biometric"}; + response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, null, + false, null, true, requiredSecondaryFactors, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(1, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(requiredSecondaryFactors, new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class)); + + // test both + requiredSecondaryFactors = new String[]{"otp-phone", "emailpassword", "biometric", "custom"}; + response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", true, null, true, + false, null, true, requiredSecondaryFactors, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertTrue(tenant.get("requiredSecondaryFactors").isJsonArray()); + assertEquals(4, tenant.get("requiredSecondaryFactors").getAsJsonArray().size()); + assertEquals(Set.of(requiredSecondaryFactors), Set.of(new Gson().fromJson(tenant.get("requiredSecondaryFactors").getAsJsonArray(), String[].class))); + + response = TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, null, + false, null, true, null, + config, SemVer.v5_0); + assertFalse(response.get("createdNew").getAsBoolean()); + + tenant = TestMultitenancyAPIHelper.getTenant(new TenantIdentifier(null, null, "t1"), + process.getProcess(), SemVer.v5_0); + assertNull(tenant.get("requiredSecondaryFactors")); + } + + @Test + public void testDuplicateValuesInFirstFactorsAndRequiredSecondaryFactors() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + JsonObject config = new JsonObject(); + StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + + String[] factors = new String[]{"duplicate", "emailpassword", "duplicate", "custom"}; + try { + TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, null, + true, factors, false, null, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: firstFactors input should not contain duplicate values", e.getMessage()); + } + + try { + TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", null, null, null, + false, null, true, factors, + config, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(400, e.statusCode); + assertEquals("Http error. Status Code: 400. Message: requiredSecondaryFactors input should not contain duplicate values", e.getMessage()); + } + + } } 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 8243b73fe..8c97dd948 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestTenantIdIsNotPresentForOlderCDI.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestTenantIdIsNotPresentForOlderCDI.java @@ -122,7 +122,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -142,7 +142,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -162,7 +162,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } 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 6830d38ae..cf7457e6d 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestTenantUserAssociation.java @@ -197,8 +197,9 @@ public void testUserDisassociationForNotAuthRecipes() throws Exception { } if (name.equals(UserMetadataStorage.class.getName()) - || name.equals(JWTRecipeStorage.class.getName()) || - name.equals(ActiveUsersStorage.class.getName())) { + || name.equals(JWTRecipeStorage.class.getName()) + || name.equals(ActiveUsersStorage.class.getName()) + ) { // user metadata is app specific and does not have any tenant specific data // JWT storage does not have any user specific data // Active users storage does not have tenant specific data diff --git a/src/test/java/io/supertokens/test/multitenant/api/TestWithNonAuthRecipes.java b/src/test/java/io/supertokens/test/multitenant/api/TestWithNonAuthRecipes.java index d755d679a..2cb01382a 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestWithNonAuthRecipes.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestWithNonAuthRecipes.java @@ -114,8 +114,8 @@ public void testThatUserMetadataIsSavedInTheStorageWhereUserExists() throws Exce AuthRecipeUserInfo user1 = EmailPassword.signUp(t0, t0Storage, process.getProcess(), "test@example.com", "password123"); AuthRecipeUserInfo user2 = EmailPassword.signUp(t1, t1Storage, process.getProcess(), "test@example.com", "password123"); - UserIdMapping.populateExternalUserIdForUsers(t0Storage, new AuthRecipeUserInfo[]{user1}); - UserIdMapping.populateExternalUserIdForUsers(t1Storage, new AuthRecipeUserInfo[]{user2}); + UserIdMapping.populateExternalUserIdForUsers(t0.toAppIdentifier(), t0Storage, new AuthRecipeUserInfo[]{user1}); + UserIdMapping.populateExternalUserIdForUsers(t0.toAppIdentifier(), t1Storage, new AuthRecipeUserInfo[]{user2}); // Check that get user by ID works fine JsonObject jsonUser1 = TestMultitenancyAPIHelper.getUserById(t0, user1.getSupertokensUserId(), process.getProcess()); @@ -134,12 +134,22 @@ public void testThatUserMetadataIsSavedInTheStorageWhereUserExists() throws Exce jsonUser2 = TestMultitenancyAPIHelper.getUserById(t0, user2.getSupertokensUserId(), process.getProcess()); assertEquals(user2.toJson(), jsonUser2.get("user").getAsJsonObject()); - jsonUser2 = TestMultitenancyAPIHelper.getUserById(t1, user2.getSupertokensUserId(), process.getProcess()); - assertEquals(user2.toJson(), jsonUser2.get("user").getAsJsonObject()); + try { + TestMultitenancyAPIHelper.getUserById(t1, user2.getSupertokensUserId(), + process.getProcess()); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } } - { // Add metadata using t1 results should be ok - TestMultitenancyAPIHelper.updateUserMetadata(t1, user1.getSupertokensUserId(), metadata, process.getProcess()); + { // Add metadata using t1 results in 403 + try { + TestMultitenancyAPIHelper.updateUserMetadata(t1, user1.getSupertokensUserId(), metadata, process.getProcess()); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } } { @@ -149,8 +159,12 @@ public void testThatUserMetadataIsSavedInTheStorageWhereUserExists() throws Exce jsonUser1 = TestMultitenancyAPIHelper.getUserById(t0, user1.getSupertokensUserId(), process.getProcess()); assertEquals(user1.toJson(), jsonUser1.get("user").getAsJsonObject()); - jsonUser1 = TestMultitenancyAPIHelper.getUserById(t1, user1.getSupertokensUserId(), process.getProcess()); - assertEquals(user1.toJson(), jsonUser1.get("user").getAsJsonObject()); + try { + TestMultitenancyAPIHelper.getUserById(t1, user1.getSupertokensUserId(), process.getProcess()); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } } UserMetadataSQLStorage t0UserMetadataStorage = StorageUtils.getUserMetadataStorage(t0Storage); @@ -165,7 +179,13 @@ public void testThatUserMetadataIsSavedInTheStorageWhereUserExists() throws Exce assertNull(t1UserMetadataStorage.getUserMetadata(t0.toAppIdentifier(), user1.getSupertokensUserId())); // ensure t1 storage does not have user1's metadata // Try deleting metadata - TestMultitenancyAPIHelper.removeMetadata(t1, user1.getSupertokensUserId(), process.getProcess()); + try { + TestMultitenancyAPIHelper.removeMetadata(t1, user1.getSupertokensUserId(), process.getProcess()); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } + TestMultitenancyAPIHelper.removeMetadata(t0, user1.getSupertokensUserId(), process.getProcess()); TestMultitenancyAPIHelper.removeMetadata(t0, user2.getSupertokensUserId(), process.getProcess()); assertNull(t0UserMetadataStorage.getUserMetadata(t0.toAppIdentifier(), user1.getSupertokensUserId())); // ensure t0 storage does not have user2's metadata assertNull(t1UserMetadataStorage.getUserMetadata(t0.toAppIdentifier(), user2.getSupertokensUserId())); // ensure t1 storage does not have user1's metadata @@ -191,8 +211,8 @@ public void testThatRoleIsStoredInPublicTenantAndUserRoleMappingInTheUserTenantS AuthRecipeUserInfo user1 = EmailPassword.signUp(t0, t0Storage, process.getProcess(), "test@example.com", "password123"); AuthRecipeUserInfo user2 = EmailPassword.signUp(t1, t1Storage, process.getProcess(), "test@example.com", "password123"); - UserIdMapping.populateExternalUserIdForUsers(t0Storage, new AuthRecipeUserInfo[]{user1}); - UserIdMapping.populateExternalUserIdForUsers(t1Storage, new AuthRecipeUserInfo[]{user2}); + UserIdMapping.populateExternalUserIdForUsers(t0.toAppIdentifier(), t0Storage, new AuthRecipeUserInfo[]{user1}); + UserIdMapping.populateExternalUserIdForUsers(t1.toAppIdentifier(), t1Storage, new AuthRecipeUserInfo[]{user2}); { // Check that get user by ID works fine @@ -205,7 +225,13 @@ public void testThatRoleIsStoredInPublicTenantAndUserRoleMappingInTheUserTenantS TestMultitenancyAPIHelper.createRole(t0, "role1", process.getProcess()); - TestMultitenancyAPIHelper.createRole(t1, "role2", process.getProcess()); + try { + TestMultitenancyAPIHelper.createRole(t1, "role2", process.getProcess()); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } + TestMultitenancyAPIHelper.createRole(t0, "role2", process.getProcess()); TestMultitenancyAPIHelper.addRoleToUser(t0, user1.getSupertokensUserId(), "role1", process.getProcess()); TestMultitenancyAPIHelper.addRoleToUser(t1, user2.getSupertokensUserId(), "role2", process.getProcess()); @@ -231,7 +257,14 @@ public void testThatRoleIsStoredInPublicTenantAndUserRoleMappingInTheUserTenantS assertEquals(1, user2Roles.get("roles").getAsJsonArray().size()); } - TestMultitenancyAPIHelper.deleteRole(t1, "role1", process.getProcess()); + try { + TestMultitenancyAPIHelper.deleteRole(t1, "role1", process.getProcess()); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } + + TestMultitenancyAPIHelper.deleteRole(t0, "role1", process.getProcess()); TestMultitenancyAPIHelper.deleteRole(t0, "role2", process.getProcess()); { @@ -276,8 +309,8 @@ public void testEmailVerificationWithUsersOnDifferentTenantStorages() throws Exc AuthRecipeUserInfo user1 = EmailPassword.signUp(t0, t0Storage, process.getProcess(), "test@example.com", "password123"); AuthRecipeUserInfo user2 = EmailPassword.signUp(t1, t1Storage, process.getProcess(), "test@example.com", "password123"); - UserIdMapping.populateExternalUserIdForUsers(t0Storage, new AuthRecipeUserInfo[]{user1}); - UserIdMapping.populateExternalUserIdForUsers(t1Storage, new AuthRecipeUserInfo[]{user2}); + UserIdMapping.populateExternalUserIdForUsers(t0.toAppIdentifier(), t0Storage, new AuthRecipeUserInfo[]{user1}); + UserIdMapping.populateExternalUserIdForUsers(t1.toAppIdentifier(), t1Storage, new AuthRecipeUserInfo[]{user2}); // Check that get user by ID works fine JsonObject jsonUser1 = TestMultitenancyAPIHelper.getUserById(t0, user1.getSupertokensUserId(), process.getProcess()); @@ -294,10 +327,13 @@ public void testEmailVerificationWithUsersOnDifferentTenantStorages() throws Exc user2.loginMethods[0].setVerified(); assertEquals(user2.toJson(), jsonUser2.get("user").getAsJsonObject()); - jsonUser2 = TestMultitenancyAPIHelper.getUserById(t1, user2.getSupertokensUserId(), process.getProcess()); - user2.loginMethods[0].setVerified(); - assertEquals(user2.toJson(), jsonUser2.get("user").getAsJsonObject()); - + try { + TestMultitenancyAPIHelper.getUserById(t1, user2.getSupertokensUserId(), + process.getProcess()); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } } { @@ -308,9 +344,12 @@ public void testEmailVerificationWithUsersOnDifferentTenantStorages() throws Exc user1.loginMethods[0].setVerified(); assertEquals(user1.toJson(), jsonUser1.get("user").getAsJsonObject()); - jsonUser1 = TestMultitenancyAPIHelper.getUserById(t1, user1.getSupertokensUserId(), process.getProcess()); - user1.loginMethods[0].setVerified(); - assertEquals(user1.toJson(), jsonUser1.get("user").getAsJsonObject()); + try { + TestMultitenancyAPIHelper.getUserById(t1, user1.getSupertokensUserId(), process.getProcess()); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } } EmailVerificationSQLStorage t0EvStorage = StorageUtils.getEmailVerificationStorage(t0Storage); @@ -325,7 +364,13 @@ public void testEmailVerificationWithUsersOnDifferentTenantStorages() throws Exc assertFalse(t1EvStorage.isEmailVerified(t0.toAppIdentifier(), user1.getSupertokensUserId(), "test@example.com")); // ensure t1 storage does not have user1's ev // Try unverify - TestMultitenancyAPIHelper.unverifyEmail(t1, user1.getSupertokensUserId(), "test@example.com", process.getProcess()); + try { + TestMultitenancyAPIHelper.unverifyEmail(t1, user1.getSupertokensUserId(), "test@example.com", process.getProcess()); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } + TestMultitenancyAPIHelper.unverifyEmail(t0, user1.getSupertokensUserId(), "test@example.com", process.getProcess()); TestMultitenancyAPIHelper.unverifyEmail(t0, user2.getSupertokensUserId(), "test@example.com", process.getProcess()); assertFalse(t1EvStorage.isEmailVerified(t0.toAppIdentifier(), user2.getSupertokensUserId(), "test@example.com")); // ensure t1 storage does not have user2's ev assertFalse(t0EvStorage.isEmailVerified(t0.toAppIdentifier(), user1.getSupertokensUserId(), "test@example.com")); // ensure t0 storage does not have user1's ev diff --git a/src/test/java/io/supertokens/test/multitenant/generator/GenerateTenantConfig.java b/src/test/java/io/supertokens/test/multitenant/generator/GenerateTenantConfig.java index a03cf0eaa..abb633f26 100644 --- a/src/test/java/io/supertokens/test/multitenant/generator/GenerateTenantConfig.java +++ b/src/test/java/io/supertokens/test/multitenant/generator/GenerateTenantConfig.java @@ -20,8 +20,51 @@ import io.supertokens.pluginInterface.multitenancy.*; import java.lang.reflect.InvocationTargetException; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; public class GenerateTenantConfig { + private static final String[] FACTORS = new String[]{ + "emailpassword1", + "thirdparty1", + "otp-email1", + "otp-phone1", + "link-email1", + "link-phone1", + "totp", + "biometric", + "custom" + }; + + private static String[] selectRandomElements(String[] inputArray) { + Random random = new Random(); + int numElementsToSelect = random.nextInt(4); // Randomly select 0 to 3 elements + + // Ensure numElementsToSelect is within the bounds of the array + numElementsToSelect = Math.min(numElementsToSelect, inputArray.length); + + // Create a set to store unique indices + Set selectedIndices = new HashSet<>(); + + // Generate random indices and select the corresponding elements + while (selectedIndices.size() < numElementsToSelect) { + int randomIndex = random.nextInt(inputArray.length); + selectedIndices.add(randomIndex); + } + + // Create an array to hold the randomly selected elements + String[] selectedElements = new String[numElementsToSelect]; + + // Fill the array with the selected elements + int i = 0; + for (int index : selectedIndices) { + selectedElements[i++] = inputArray[index]; + } + + return selectedElements; + } + public static ConfigGenerator.GeneratedValueAndExpectation generate_tenantIdentifier() { // TODO: generate different appid and tenantid return new ConfigGenerator.GeneratedValueAndExpectation( @@ -48,6 +91,32 @@ public static ConfigGenerator.GeneratedValueAndExpectation generate_thirdPartyCo return ConfigGenerator.generate(ThirdPartyConfig.class); } + public static ConfigGenerator.GeneratedValueAndExpectation generate_firstFactors() { + if (new Random().nextFloat() < 0.15) { + return new ConfigGenerator.GeneratedValueAndExpectation( + null, + new ConfigGenerator.Expectation("ok", null)); + } + + String[] factors = selectRandomElements(FACTORS); + return new ConfigGenerator.GeneratedValueAndExpectation( + factors, + new ConfigGenerator.Expectation("ok", factors)); + } + + public static ConfigGenerator.GeneratedValueAndExpectation generate_requiredSecondaryFactors() { + if (new Random().nextFloat() < 0.15) { + return new ConfigGenerator.GeneratedValueAndExpectation( + null, + new ConfigGenerator.Expectation("ok", null)); + } + + String[] factors = selectRandomElements(FACTORS); + return new ConfigGenerator.GeneratedValueAndExpectation( + factors, + new ConfigGenerator.Expectation("ok", factors)); + } + public static ConfigGenerator.GeneratedValueAndExpectation generate_coreConfig() { // TODO: return new ConfigGenerator.GeneratedValueAndExpectation(new JsonObject(), new ConfigGenerator.Expectation("ok", new JsonObject())); diff --git a/src/test/java/io/supertokens/test/passwordless/api/MultitenantAPITest.java b/src/test/java/io/supertokens/test/passwordless/api/MultitenantAPITest.java index 913366fd4..028e0dae1 100644 --- a/src/test/java/io/supertokens/test/passwordless/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/passwordless/api/MultitenantAPITest.java @@ -108,6 +108,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -128,6 +129,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -148,6 +150,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); diff --git a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessCheckCodeAPITest5_0.java b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessCheckCodeAPITest5_0.java new file mode 100644 index 000000000..4d8b9fe42 --- /dev/null +++ b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessCheckCodeAPITest5_0.java @@ -0,0 +1,991 @@ +/* + * 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.passwordless.api; + +import com.google.gson.JsonObject; +import io.supertokens.ActiveUsers; +import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.Passwordless.CreateCodeResponse; +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.test.httpRequest.HttpResponseException; +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 PasswordlessCheckCodeAPITest5_0 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @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; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Field name 'preAuthSessionId' is invalid in JSON input", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash + "asdf"); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: preAuthSessionId and deviceId doesn't match", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash + "asdf"); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: preAuthSessionId and deviceId doesn't match", + error.getMessage()); + } + + /* + * malformed linkCode -> BadRequest + */ + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode + "==#"); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: Input encoding error in linkCode", error.getMessage()); + } + + /* + * malformed deviceId -> BadRequest + * TODO: throwing 500 error + */ + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId + "==#"); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testLinkCode() 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(); + + String email = "test@example.com"; + 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/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testExpiredLinkCode() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_code_lifetime", "100"); + + 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Thread.sleep(150); + 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/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("RESTART_FLOW_ERROR", response.get("status").getAsString()); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUserInputCode() 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUserInputCodeDoesNotDeleteTheCode() 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, email, null); + + // should be able to call again, if the code is not deleted + response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testExpiredUserInputCode() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_code_lifetime", "100"); + 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Thread.sleep(150); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("EXPIRED_USER_INPUT_CODE_ERROR", response.get("status").getAsString()); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testIncorrectUserInputCode() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_max_code_input_attempts", "2"); // Only 2 code entries permitted (1 retry) + + 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"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode + "nope"); + + { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("INCORRECT_USER_INPUT_CODE_ERROR", response.get("status").getAsString()); + } + + { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("RESTART_FLOW_ERROR", response.get("status").getAsString()); + } + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testConsumeCodeWithoutCreatingUser() 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + assertEquals(0, AuthRecipe.getUsersCount(process.getProcess(), null)); // ensure that no user was actually created + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testVerifyCodeReturnsUserIfItAlreadyExists() 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Passwordless.consumeCode(process.getProcess(), createResp.deviceId, createResp.deviceIdHash, + createResp.userInputCode, null); + + createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testBadInputWithoutCreatingUser() 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Field name 'preAuthSessionId' is invalid in JSON input", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash + "asdf"); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: preAuthSessionId and deviceId doesn't match", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash + "asdf"); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: preAuthSessionId and deviceId doesn't match", + error.getMessage()); + } + + /* + * malformed linkCode -> BadRequest + */ + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode + "==#"); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: Input encoding error in linkCode", error.getMessage()); + } + + /* + * malformed deviceId -> BadRequest + * TODO: throwing 500 error + */ + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId + "==#"); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testLinkCodeWithoutCreatingUser() 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(); + + String email = "test@example.com"; + 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/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testExpiredLinkCodeWithoutCreatingUser() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_code_lifetime", "100"); + + 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Thread.sleep(150); + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("RESTART_FLOW_ERROR", response.get("status").getAsString()); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testExpiredUserInputCodeWithoutCreatingUser() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_code_lifetime", "100"); + 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Thread.sleep(150); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("EXPIRED_USER_INPUT_CODE_ERROR", response.get("status").getAsString()); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testIncorrectUserInputCodeWithoutCreatingUser() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_max_code_input_attempts", "2"); // Only 2 code entries permitted (1 retry) + + 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"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode + "nope"); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("INCORRECT_USER_INPUT_CODE_ERROR", response.get("status").getAsString()); + } + + { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/check", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("RESTART_FLOW_ERROR", response.get("status").getAsString()); + } + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private void checkResponse(JsonObject response, String email, String phoneNumber) { + assertEquals("OK", response.get("status").getAsString()); + + assertEquals(2, response.entrySet().size()); + assertTrue(response.has("consumedDevice")); + + JsonObject consumedDevice = response.get("consumedDevice").getAsJsonObject(); + assertEquals(3, consumedDevice.entrySet().size()); + assertTrue(consumedDevice.has("preAuthSessionId")); + assertTrue(consumedDevice.has("failedCodeInputAttemptCount")); + + if (email != null) { + assertEquals(email, consumedDevice.get("email").getAsString()); + } else { + assertTrue(consumedDevice.get("phoneNumber").isJsonNull()); + } + + } +} diff --git a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java new file mode 100644 index 000000000..b52fde519 --- /dev/null +++ b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java @@ -0,0 +1,495 @@ +/* + * 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.passwordless.api; + +import com.google.gson.JsonObject; +import io.supertokens.ActiveUsers; +import io.supertokens.ProcessState; +import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.Passwordless.CreateCodeResponse; +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.test.httpRequest.HttpResponseException; +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 PasswordlessConsumeCodeAPITest5_0 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @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; + } + + long startTs = System.currentTimeMillis(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Field name 'preAuthSessionId' is invalid in JSON input", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash + "asdf"); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: preAuthSessionId and deviceId doesn't match", + error.getMessage()); + } + + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash + "asdf"); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: preAuthSessionId and deviceId doesn't match", + error.getMessage()); + } + + /* + * malformed linkCode -> BadRequest + */ + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("linkCode", createResp.linkCode + "==#"); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: Input encoding error in linkCode", error.getMessage()); + } + + /* + * malformed deviceId -> BadRequest + * TODO: throwing 500 error + */ + { + HttpResponseException error = null; + try { + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId + "==#"); + + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals( + "Http error. Status Code: 400. Message: Please provide exactly one of linkCode or deviceId+userInputCode", + error.getMessage()); + } + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testLinkCode() 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(); + + String email = "test@example.com"; + 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.v5_0.get(), "passwordless"); + + checkResponse(response, true, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testExpiredLinkCode() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_code_lifetime", "100"); + + 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Thread.sleep(150); + 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.v5_0.get(), "passwordless"); + + assertEquals("RESTART_FLOW_ERROR", response.get("status").getAsString()); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testUserInputCode() 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, true, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 1); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testExpiredUserInputCode() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_code_lifetime", "100"); + 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(); + + String email = "test@example.com"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + Thread.sleep(150); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("EXPIRED_USER_INPUT_CODE_ERROR", response.get("status").getAsString()); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 0); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testIncorrectUserInputCode() throws Exception { + String[] args = { "../" }; + + Utils.setValueInConfig("passwordless_max_code_input_attempts", "2"); // Only 2 code entries permitted (1 retry) + + 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"; + CreateCodeResponse createResp = Passwordless.createCode(process.getProcess(), email, null, null, null); + + JsonObject consumeCodeRequestBody = new JsonObject(); + consumeCodeRequestBody.addProperty("deviceId", createResp.deviceId); + consumeCodeRequestBody.addProperty("preAuthSessionId", createResp.deviceIdHash); + consumeCodeRequestBody.addProperty("userInputCode", createResp.userInputCode + "nope"); + + { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("INCORRECT_USER_INPUT_CODE_ERROR", response.get("status").getAsString()); + } + + { + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("RESTART_FLOW_ERROR", response.get("status").getAsString()); + } + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private void checkResponse(JsonObject response, Boolean isNewUser, String email, String phoneNumber) { + assertEquals("OK", response.get("status").getAsString()); + assertEquals(isNewUser, response.get("createdNewUser").getAsBoolean()); + assert (response.has("user")); + + assertEquals(5, response.entrySet().size()); + + JsonObject userJson = response.getAsJsonObject("user"); + if (email != null) { + assertEquals(email, userJson.get("emails").getAsJsonArray().get(0).getAsString()); + } + + if (phoneNumber != null) { + assertEquals(phoneNumber, userJson.get("phoneNumbers").getAsJsonArray().get(0).getAsString()); + } + assertEquals(8, userJson.entrySet().size()); + + assertTrue(response.has("recipeUserId")); + assertTrue(response.has("consumedDevice")); + + JsonObject consumedDevice = response.getAsJsonObject("consumedDevice"); + assertEquals(3, consumedDevice.entrySet().size()); + assertTrue(consumedDevice.has("preAuthSessionId")); + assertTrue(consumedDevice.has("failedCodeInputAttemptCount")); + if (email != null) { + assertEquals(email, consumedDevice.get("email").getAsString()); + } else { + assertEquals(phoneNumber, consumedDevice.get("phoneNumber").getAsString()); + } + } +} diff --git a/src/test/java/io/supertokens/test/passwordless/api/PasswordlessDeleteCodeAPITest5_0.java b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessDeleteCodeAPITest5_0.java new file mode 100644 index 000000000..9270d05b5 --- /dev/null +++ b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessDeleteCodeAPITest5_0.java @@ -0,0 +1,215 @@ +/* + * 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.passwordless.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.passwordless.PasswordlessCode; +import io.supertokens.pluginInterface.passwordless.sqlStorage.PasswordlessSQLStorage; +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.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 PasswordlessDeleteCodeAPITest5_0 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testDeleteCode() 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; + } + + PasswordlessSQLStorage storage = (PasswordlessSQLStorage) StorageLayer.getStorage(process.getProcess()); + + String phoneNumber = "+442071838750"; + String codeId = "codeId"; + + String deviceIdHash = "pZ9SP0USbXbejGFO6qx7x3JBjupJZVtw4RkFiNtJGqc"; + String linkCodeHash = "wo5UcFFVSblZEd1KOUOl-dpJ5zpSr_Qsor1Eg4TzDRE"; + + storage.createDeviceWithCode(new TenantIdentifier(null, null, null), null, phoneNumber, "linkCodeSalt", + new PasswordlessCode(codeId, deviceIdHash, linkCodeHash, System.currentTimeMillis())); + + JsonObject createCodeRequestBody = new JsonObject(); + createCodeRequestBody.addProperty("codeId", codeId); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/remove", createCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("OK", response.get("status").getAsString()); + + assertNull(storage.getCode(new TenantIdentifier(null, null, null), codeId)); + assertNull(storage.getDevice(new TenantIdentifier(null, null, null), deviceIdHash)); + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testDeleteNonExistentCode() 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 codeId = "codeId"; + + JsonObject createCodeRequestBody = new JsonObject(); + createCodeRequestBody.addProperty("codeId", codeId); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/remove", createCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("OK", response.get("status").getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + /** + * empty request body -> BadRequest + * + * @throws Exception + */ + @Test + public void testEmptyRequestBody() 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 createCodeRequestBody = new JsonObject(); + + HttpResponseException error = null; + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/remove", createCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + } catch (HttpResponseException ex) { + error = ex; + } + + assertNotNull(error); + assertEquals(400, error.statusCode); + assertEquals("Http error. Status Code: 400. Message: Please provide either 'codeId' or 'preAuthSessionId'", + error.getMessage()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testDeleteNonExistantDeviceIdHash() 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 preAuthSessionId = "preAuthSessionId"; + + JsonObject createCodeRequestBody = new JsonObject(); + createCodeRequestBody.addProperty("preAuthSessionId", preAuthSessionId); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/remove", createCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("OK", response.get("status").getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testDeleteDeviceIdHash() 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; + } + + PasswordlessSQLStorage storage = (PasswordlessSQLStorage) StorageLayer.getStorage(process.getProcess()); + + String phoneNumber = "+442071838750"; + String codeId = "codeId"; + + String deviceIdHash = "pZ9SP0USbXbejGFO6qx7x3JBjupJZVtw4RkFiNtJGqc"; + String linkCodeHash = "wo5UcFFVSblZEd1KOUOl-dpJ5zpSr_Qsor1Eg4TzDRE"; + + storage.createDeviceWithCode(new TenantIdentifier(null, null, null), null, phoneNumber, "linkCodeSalt", + new PasswordlessCode(codeId, deviceIdHash, linkCodeHash, System.currentTimeMillis())); + + JsonObject createCodeRequestBody = new JsonObject(); + createCodeRequestBody.addProperty("preAuthSessionId", deviceIdHash); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup/code/remove", createCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals("OK", response.get("status").getAsString()); + + assertNull(storage.getCode(new TenantIdentifier(null, null, null), codeId)); + assertNull(storage.getDevice(new TenantIdentifier(null, null, null), deviceIdHash)); + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} diff --git a/src/test/java/io/supertokens/test/session/SessionTest6.java b/src/test/java/io/supertokens/test/session/SessionTest6.java new file mode 100644 index 000000000..6a8f6c89d --- /dev/null +++ b/src/test/java/io/supertokens/test/session/SessionTest6.java @@ -0,0 +1,203 @@ +/* + * 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.session; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.exceptions.TryRefreshTokenException; +import io.supertokens.exceptions.UnauthorisedException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.session.SessionStorage; +import io.supertokens.session.Session; +import io.supertokens.session.accessToken.AccessToken; +import io.supertokens.session.info.SessionInformationHolder; +import io.supertokens.session.jwt.JWT; +import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import org.junit.*; +import org.junit.rules.TestRule; + +import static junit.framework.TestCase.*; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +public class SessionTest6 { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void createRefreshSwitchVerify() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + SessionInformationHolder sessionInfo = Session.createNewSession(process.getProcess(), userId, userDataInJWT, + userDataInDatabase, false, AccessToken.getLatestVersion(), false); + checkIfUsingStaticKey(sessionInfo, false); + + sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token, + sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), true); + assert sessionInfo.refreshToken != null; + assert sessionInfo.accessToken != null; + + checkIfUsingStaticKey(sessionInfo, true); + + SessionInformationHolder verifiedSession = Session.getSession(process.getProcess(), sessionInfo.accessToken.token, + sessionInfo.antiCsrfToken, false, true, false); + + checkIfUsingStaticKey(verifiedSession, true); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + @Test + public void createRefreshSwitchRegen() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + SessionInformationHolder sessionInfo = Session.createNewSession(process.getProcess(), userId, userDataInJWT, + userDataInDatabase, false, AccessToken.getLatestVersion(), false); + checkIfUsingStaticKey(sessionInfo, false); + + sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token, + sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), true); + assert sessionInfo.refreshToken != null; + assert sessionInfo.accessToken != null; + checkIfUsingStaticKey(sessionInfo, true); + + SessionInformationHolder newSessionInfo = Session.regenerateToken(process.getProcess(), + sessionInfo.accessToken.token, userDataInJWT); + checkIfUsingStaticKey(newSessionInfo, true); + + SessionInformationHolder getSessionResponse = Session.getSession(process.getProcess(), + newSessionInfo.accessToken.token, sessionInfo.antiCsrfToken, false, true, false); + checkIfUsingStaticKey(getSessionResponse, true); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void createRefreshRefreshSwitchVerify() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + SessionInformationHolder sessionInfo = Session.createNewSession(process.getProcess(), userId, userDataInJWT, + userDataInDatabase, false, AccessToken.getLatestVersion(), false); + checkIfUsingStaticKey(sessionInfo, false); + + sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token, + sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), false); + + sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token, + sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), true); + assert sessionInfo.refreshToken != null; + assert sessionInfo.accessToken != null; + + checkIfUsingStaticKey(sessionInfo, true); + + SessionInformationHolder verifiedSession = Session.getSession(process.getProcess(), sessionInfo.accessToken.token, + sessionInfo.antiCsrfToken, false, true, false); + + checkIfUsingStaticKey(verifiedSession, true); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + @Test + public void createRefreshRefreshSwitchRegen() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + SessionInformationHolder sessionInfo = Session.createNewSession(process.getProcess(), userId, userDataInJWT, + userDataInDatabase, false, AccessToken.getLatestVersion(), false); + checkIfUsingStaticKey(sessionInfo, false); + + sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token, + sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), false); + + sessionInfo = Session.refreshSession(new AppIdentifier(null, null), process.getProcess(), sessionInfo.refreshToken.token, + sessionInfo.antiCsrfToken, false, AccessToken.getLatestVersion(), true); + assert sessionInfo.refreshToken != null; + assert sessionInfo.accessToken != null; + checkIfUsingStaticKey(sessionInfo, true); + + SessionInformationHolder newSessionInfo = Session.regenerateToken(process.getProcess(), + sessionInfo.accessToken.token, userDataInJWT); + checkIfUsingStaticKey(newSessionInfo, true); + + SessionInformationHolder getSessionResponse = Session.getSession(process.getProcess(), + newSessionInfo.accessToken.token, sessionInfo.antiCsrfToken, false, true, false); + checkIfUsingStaticKey(getSessionResponse, true); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + private static void checkIfUsingStaticKey(SessionInformationHolder info, boolean shouldBeStatic) throws JWT.JWTException { + assert info.accessToken != null; + JWT.JWTPreParseInfo tokenInfo = JWT.preParseJWTInfo(info.accessToken.token); + assert tokenInfo.kid != null; + if (shouldBeStatic) { + assert tokenInfo.kid.startsWith("s-"); + } else { + assert tokenInfo.kid.startsWith("d-"); + } + } + +} + diff --git a/src/test/java/io/supertokens/test/session/api/MultitenantAPITest.java b/src/test/java/io/supertokens/test/session/api/MultitenantAPITest.java index 51c860957..6b18da6c9 100644 --- a/src/test/java/io/supertokens/test/session/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/session/api/MultitenantAPITest.java @@ -109,6 +109,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -129,6 +130,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -149,6 +151,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); diff --git a/src/test/java/io/supertokens/test/session/api/RefreshSessionAPITest2_21.java b/src/test/java/io/supertokens/test/session/api/RefreshSessionAPITest2_21.java index 07738e235..9be3bbcd7 100644 --- a/src/test/java/io/supertokens/test/session/api/RefreshSessionAPITest2_21.java +++ b/src/test/java/io/supertokens/test/session/api/RefreshSessionAPITest2_21.java @@ -76,7 +76,7 @@ public void checkRefreshWithProtectedFieldInPayload() throws Exception { JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null, - Utils.getCdiVersionStringLatestForTests(), "session"); + SemVer.v2_21.get(), "session"); assertEquals(response.entrySet().size(), 2); assertEquals(response.get("status").getAsString(), "UNAUTHORISED"); diff --git a/src/test/java/io/supertokens/test/session/api/RefreshSessionAPITest5_0.java b/src/test/java/io/supertokens/test/session/api/RefreshSessionAPITest5_0.java new file mode 100644 index 000000000..e29f9b03b --- /dev/null +++ b/src/test/java/io/supertokens/test/session/api/RefreshSessionAPITest5_0.java @@ -0,0 +1,334 @@ +/* + * 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.session.api; + +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.session.jwt.JWT; +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 junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +public class RefreshSessionAPITest5_0 { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void successOutputUpgradeWithNonStaticKeySessionTest() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.add("nullProp", JsonNull.INSTANCE); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + + JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session", request, 1000, 1000, null, SemVer.v2_7.get(), + "session"); + assertEquals(sessionInfo.get("status").getAsString(), "OK"); + + JsonObject sessionRefreshBody = new JsonObject(); + + sessionRefreshBody.addProperty("refreshToken", + sessionInfo.get("refreshToken").getAsJsonObject().get("token").getAsString()); + sessionRefreshBody.addProperty("enableAntiCsrf", false); + sessionRefreshBody.addProperty("useDynamicSigningKey", true); + + JsonObject sessionRefreshResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null, + SemVer.v5_0.get(), "session"); + + checkRefreshSessionResponse(sessionRefreshResponse, process, userId, userDataInJWT, false, false); + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void successOutputUpgradeWithStaticKeySessionTest() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.add("nullProp", JsonNull.INSTANCE); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + + JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session", request, 1000, 1000, null, SemVer.v2_7.get(), + "session"); + assertEquals(sessionInfo.get("status").getAsString(), "OK"); + + JsonObject sessionRefreshBody = new JsonObject(); + + sessionRefreshBody.addProperty("refreshToken", + sessionInfo.get("refreshToken").getAsJsonObject().get("token").getAsString()); + sessionRefreshBody.addProperty("enableAntiCsrf", false); + sessionRefreshBody.addProperty("useDynamicSigningKey", false); + + JsonObject sessionRefreshResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null, + SemVer.v5_0.get(), "session"); + + checkRefreshSessionResponse(sessionRefreshResponse, process, userId, userDataInJWT, false, true); + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void successOutputWithStaticKeySessionTest() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.add("nullProp", JsonNull.INSTANCE); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + + JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session", request, 1000, 1000, null, SemVer.v5_0.get(), + "session"); + assertEquals(sessionInfo.get("status").getAsString(), "OK"); + + JsonObject sessionRefreshBody = new JsonObject(); + + sessionRefreshBody.addProperty("refreshToken", + sessionInfo.get("refreshToken").getAsJsonObject().get("token").getAsString()); + sessionRefreshBody.addProperty("enableAntiCsrf", false); + sessionRefreshBody.addProperty("useDynamicSigningKey", false); + + JsonObject sessionRefreshResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null, + SemVer.v5_0.get(), "session"); + + checkRefreshSessionResponse(sessionRefreshResponse, process, userId, userDataInJWT, false, true); + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void successOutputWithNonStaticKeySessionTest() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.add("nullProp", JsonNull.INSTANCE); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + + JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session", request, 1000, 1000, null, SemVer.v5_0.get(), + "session"); + assertEquals(sessionInfo.get("status").getAsString(), "OK"); + + JsonObject sessionRefreshBody = new JsonObject(); + + sessionRefreshBody.addProperty("refreshToken", + sessionInfo.get("refreshToken").getAsJsonObject().get("token").getAsString()); + sessionRefreshBody.addProperty("enableAntiCsrf", false); + sessionRefreshBody.addProperty("useDynamicSigningKey", false); + + JsonObject sessionRefreshResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null, + SemVer.v5_0.get(), "session"); + + checkRefreshSessionResponse(sessionRefreshResponse, process, userId, userDataInJWT, false, true); + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void successOutputSwitchingWithStaticKeySessionTest() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.add("nullProp", JsonNull.INSTANCE); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("enableAntiCsrf", false); + request.addProperty("useDynamicSigningKey", false); + + JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session", request, 1000, 1000, null, SemVer.v5_0.get(), + "session"); + assertEquals(sessionInfo.get("status").getAsString(), "OK"); + + JsonObject sessionRefreshBody = new JsonObject(); + + sessionRefreshBody.addProperty("refreshToken", + sessionInfo.get("refreshToken").getAsJsonObject().get("token").getAsString()); + sessionRefreshBody.addProperty("enableAntiCsrf", false); + sessionRefreshBody.addProperty("useDynamicSigningKey", false); + + JsonObject sessionRefreshResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null, + SemVer.v5_0.get(), "session"); + + checkRefreshSessionResponse(sessionRefreshResponse, process, userId, userDataInJWT, false, true); + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void successOutputSwitchingWithNonStaticKeySessionTest() throws Exception { + String[] args = { "../" }; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String userId = "userId"; + JsonObject userDataInJWT = new JsonObject(); + userDataInJWT.add("nullProp", JsonNull.INSTANCE); + userDataInJWT.addProperty("key", "value"); + JsonObject userDataInDatabase = new JsonObject(); + userDataInDatabase.addProperty("key", "value"); + + JsonObject request = new JsonObject(); + request.addProperty("userId", userId); + request.add("userDataInJWT", userDataInJWT); + request.add("userDataInDatabase", userDataInDatabase); + request.addProperty("useDynamicSigningKey", true); + request.addProperty("enableAntiCsrf", false); + + JsonObject sessionInfo = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session", request, 1000, 1000, null, SemVer.v5_0.get(), + "session"); + assertEquals(sessionInfo.get("status").getAsString(), "OK"); + + JsonObject sessionRefreshBody = new JsonObject(); + + sessionRefreshBody.addProperty("refreshToken", + sessionInfo.get("refreshToken").getAsJsonObject().get("token").getAsString()); + sessionRefreshBody.addProperty("enableAntiCsrf", false); + sessionRefreshBody.addProperty("useDynamicSigningKey", false); + + JsonObject sessionRefreshResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null, + SemVer.v5_0.get(), "session"); + + checkRefreshSessionResponse(sessionRefreshResponse, process, userId, userDataInJWT, false, true); + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + private static void checkRefreshSessionResponse(JsonObject response, TestingProcessManager.TestingProcess process, + String userId, JsonObject userDataInJWT, boolean hasAntiCsrf, boolean useStaticKey) throws + JWT.JWTException { + + assertNotNull(response.get("session").getAsJsonObject().get("handle").getAsString()); + assertEquals(response.get("session").getAsJsonObject().get("userId").getAsString(), userId); + assertEquals(response.get("session").getAsJsonObject().get("recipeUserId").getAsString(), userId); + assertEquals(response.get("session").getAsJsonObject().get("tenantId").getAsString(), "public"); + assertEquals(response.get("session").getAsJsonObject().get("userDataInJWT").getAsJsonObject().toString(), + userDataInJWT.toString()); + assertEquals(response.get("session").getAsJsonObject().entrySet().size(), 5); + + assertTrue(response.get("accessToken").getAsJsonObject().has("token")); + assertTrue(response.get("accessToken").getAsJsonObject().has("expiry")); + assertTrue(response.get("accessToken").getAsJsonObject().has("createdTime")); + assertEquals(response.get("accessToken").getAsJsonObject().entrySet().size(), 3); + + JWT.JWTPreParseInfo tokenInfo = JWT.preParseJWTInfo(response.get("accessToken").getAsJsonObject().get("token").getAsString()); + + if (useStaticKey) { + assert(tokenInfo.kid.startsWith("s-")); + } else { + assert(tokenInfo.kid.startsWith("d-")); + } + + assertTrue(response.get("refreshToken").getAsJsonObject().has("token")); + assertTrue(response.get("refreshToken").getAsJsonObject().has("expiry")); + assertTrue(response.get("refreshToken").getAsJsonObject().has("createdTime")); + assertEquals(response.get("refreshToken").getAsJsonObject().entrySet().size(), 3); + + assertEquals(response.has("antiCsrfToken"), hasAntiCsrf); + + assertEquals(response.entrySet().size(), hasAntiCsrf ? 5 : 4); + } + +} diff --git a/src/test/java/io/supertokens/test/session/api/SessionRegenerateAPITest2_21.java b/src/test/java/io/supertokens/test/session/api/SessionRegenerateAPITest2_21.java index 05e2f97a3..488c3f8f0 100644 --- a/src/test/java/io/supertokens/test/session/api/SessionRegenerateAPITest2_21.java +++ b/src/test/java/io/supertokens/test/session/api/SessionRegenerateAPITest2_21.java @@ -107,6 +107,7 @@ public void testCallRegenerateAPIWithProtectedFieldInJWTV3Token() throws Excepti sessionRefreshBody.addProperty("refreshToken", sessionInfo.get("refreshToken").getAsJsonObject().get("token").getAsString()); sessionRefreshBody.addProperty("enableAntiCsrf", false); + sessionRefreshBody.addProperty("useDynamicSigningKey", true); JsonObject sessionRefreshResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", "http://localhost:3567/recipe/session/refresh", sessionRefreshBody, 1000, 1000, null, diff --git a/src/test/java/io/supertokens/test/thirdparty/api/MultitenantAPITest.java b/src/test/java/io/supertokens/test/thirdparty/api/MultitenantAPITest.java index b638f860d..53a2b9235 100644 --- a/src/test/java/io/supertokens/test/thirdparty/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/thirdparty/api/MultitenantAPITest.java @@ -108,6 +108,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(true, null), new PasswordlessConfig(false), + null, null, config ) ); @@ -128,6 +129,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(true, null), new PasswordlessConfig(false), + null, null, config ) ); @@ -148,6 +150,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(true, null), new PasswordlessConfig(false), + null, null, config ) ); diff --git a/src/test/java/io/supertokens/test/totp/TOTPRecipeTest.java b/src/test/java/io/supertokens/test/totp/TOTPRecipeTest.java index 0af4feaf5..9e809d86e 100644 --- a/src/test/java/io/supertokens/test/totp/TOTPRecipeTest.java +++ b/src/test/java/io/supertokens/test/totp/TOTPRecipeTest.java @@ -23,19 +23,16 @@ import io.supertokens.cronjobs.deleteExpiredTotpTokens.DeleteExpiredTotpTokens; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; -import io.supertokens.featureflag.exceptions.InvalidLicenseKeyException; -import io.supertokens.httpRequest.HttpResponseException; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; -import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.totp.TOTPDevice; import io.supertokens.pluginInterface.totp.TOTPStorage; import io.supertokens.pluginInterface.totp.TOTPUsedCode; import io.supertokens.pluginInterface.totp.exception.DeviceAlreadyExistsException; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; +import io.supertokens.pluginInterface.totp.exception.UnknownTotpUserIdException; import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; @@ -58,8 +55,7 @@ import java.time.Instant; import java.util.Objects; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; +import static org.junit.Assert.*; // TODO: Add test for UsedCodeAlreadyExistsException once we implement time mocking @@ -101,7 +97,7 @@ public TestSetupResult defaultInit() TOTPStorage storage = (TOTPStorage) StorageLayer.getStorage(process.getProcess()); FeatureFlagTestContent.getInstance(process.main) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.TOTP}); + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MFA}); return new TestSetupResult(storage, process); } @@ -114,7 +110,7 @@ public static String generateTotpCode(Main main, TOTPDevice device) /** * Generates TOTP code similar to apps like Google Authenticator and Authy */ - private static String generateTotpCode(Main main, TOTPDevice device, int step) + public static String generateTotpCode(Main main, TOTPDevice device, int step) throws InvalidKeyException, StorageQueryException { final TimeBasedOneTimePasswordGenerator totp = new TimeBasedOneTimePasswordGenerator( Duration.ofSeconds(device.period)); @@ -150,6 +146,10 @@ public void createDeviceTest() throws Exception { TOTPDevice device = Totp.registerDevice(main, "user", "device1", 1, 30); assert !Objects.equals(device.secretKey, ""); + // Verify device + String validTotp = TOTPRecipeTest.generateTotpCode(main, device); + Totp.verifyDevice(main, "user", "device1", validTotp); + // Create same device again (should fail) assertThrows(DeviceAlreadyExistsException.class, () -> Totp.registerDevice(main, "user", "device1", 1, 30)); @@ -163,76 +163,92 @@ public void createDeviceAndVerifyCodeTest() throws Exception { } Main main = result.process.getProcess(); - // Create device + // Create devices TOTPDevice device = Totp.registerDevice(main, "user", "device1", 1, 1); + TOTPDevice unverifiedDevice = Totp.registerDevice(main, "user", "unverified-device", 1, 1); + + // Verify device: + Totp.verifyDevice(main, "user", device.deviceName, generateTotpCode(main, device, -1)); // Try login with non-existent user: - assertThrows(TotpNotEnabledException.class, - () -> Totp.verifyCode(main, "non-existent-user", "any-code", true)); + assertThrows(UnknownTotpUserIdException.class, + () -> Totp.verifyCode(main, "non-existent-user", "any-code")); - // {Code: [INVALID, VALID]} * {Devices: [VERIFIED_ONLY, ALL]} + // {Code: [INVALID, VALID]} * {Devices: [verified, unverfied]} - // Invalid code & allowUnverifiedDevice = true: + // Invalid code & unverified device: assertThrows(InvalidTotpException.class, - () -> Totp.verifyCode(main, "user", "invalid", true)); + () -> Totp.verifyCode(main, "user", "invalid")); - // Invalid code & allowUnverifiedDevice = false: + // Invalid code & verified device: assertThrows(InvalidTotpException.class, - () -> Totp.verifyCode(main, "user", "invalid", false)); + () -> Totp.verifyCode(main, "user", "invalid")); - // Valid code & allowUnverifiedDevice = false: + // Valid code & unverified device: assertThrows( InvalidTotpException.class, - () -> Totp.verifyCode(main, "user", generateTotpCode(main, device), false)); + () -> Totp.verifyCode(main, "user", generateTotpCode(main, unverifiedDevice))); + + Thread.sleep(1000 - System.currentTimeMillis() % 1000 + 10); - // Valid code & allowUnverifiedDevice = true (Success): + // Valid code & verified device (Success) String validCode = generateTotpCode(main, device); - Totp.verifyCode(main, "user", validCode, true); + Totp.verifyCode(main, "user", validCode); // Now try again with same code: assertThrows( InvalidTotpException.class, - () -> Totp.verifyCode(main, "user", validCode, true)); + () -> Totp.verifyCode(main, "user", validCode)); // Sleep for 1s so that code changes. - Thread.sleep(1000); + Thread.sleep(1000 - System.currentTimeMillis() % 1000 + 10); // Use a new valid code: String newValidCode = generateTotpCode(main, device); - Totp.verifyCode(main, "user", newValidCode, true); + Totp.verifyCode(main, "user", newValidCode); // Reuse the same code and use it again (should fail): assertThrows(InvalidTotpException.class, - () -> Totp.verifyCode(main, "user", newValidCode, true)); + () -> Totp.verifyCode(main, "user", newValidCode)); // Use a code from next period: + Thread.sleep(1); String nextValidCode = generateTotpCode(main, device, 1); - Totp.verifyCode(main, "user", nextValidCode, true); + Totp.verifyCode(main, "user", nextValidCode); // Use previous period code (should fail coz validCode has been used): + Thread.sleep(1); String previousCode = generateTotpCode(main, device, -1); assert previousCode.equals(validCode); - assertThrows(InvalidTotpException.class, () -> Totp.verifyCode(main, "user", previousCode, true)); + assertThrows(InvalidTotpException.class, () -> Totp.verifyCode(main, "user", previousCode)); // Create device with skew = 0, check that it only works with the current code + Thread.sleep(1); TOTPDevice device2 = Totp.registerDevice(main, "user", "device2", 0, 1); assert !Objects.equals(device2.secretKey, device.secretKey); + Totp.verifyDevice(main, "user", device2.deviceName, generateTotpCode(main, device2)); + + // Sleep because code was used for verifying the device + Thread.sleep(1000); String nextValidCode2 = generateTotpCode(main, device2, 1); assertThrows(InvalidTotpException.class, - () -> Totp.verifyCode(main, "user", nextValidCode2, true)); + () -> Totp.verifyCode(main, "user", nextValidCode2)); String previousValidCode2 = generateTotpCode(main, device2, -1); + Thread.sleep(1); assertThrows(InvalidTotpException.class, - () -> Totp.verifyCode(main, "user", previousValidCode2, true)); + () -> Totp.verifyCode(main, "user", previousValidCode2)); + Thread.sleep(1); String currentValidCode2 = generateTotpCode(main, device2); - Totp.verifyCode(main, "user", currentValidCode2, true); + Totp.verifyCode(main, "user", currentValidCode2); // Submit invalid code and check that it's expiry time is correct // created - expiryTime = max of ((2 * skew + 1) * period) for all devices + Thread.sleep(1); assertThrows(InvalidTotpException.class, - () -> Totp.verifyCode(main, "user", "invalid", true)); + () -> Totp.verifyCode(main, "user", "invalid")); TOTPUsedCode[] usedCodes = getAllUsedCodesUtil(result.storage, "user"); TOTPUsedCode latestCode = usedCodes[0]; @@ -247,20 +263,18 @@ public void createDeviceAndVerifyCodeTest() throws Exception { Totp.verifyDevice(main, "user", device2.deviceName, generateTotpCode(main, device2)); // device1: unverified, device2: verified - // Valid code & allowUnverifiedDevice = false: - assertThrows( - InvalidTotpException.class, - () -> Totp.verifyCode(main, "user", generateTotpCode(main, device), false)); + // Valid code & verified device: + Totp.verifyCode(main, "user", generateTotpCode(main, device)); Thread.sleep(1000); - Totp.verifyCode(main, "user", generateTotpCode(main, device2), false); + Totp.verifyCode(main, "user", generateTotpCode(main, device2)); // Valid code & allowUnverifiedDevice = true: Thread.sleep(1000); - Totp.verifyCode(main, "user", generateTotpCode(main, device), true); + Totp.verifyCode(main, "user", generateTotpCode(main, device)); Thread.sleep(1000); - Totp.verifyCode(main, "user", generateTotpCode(main, device2), true); + Totp.verifyCode(main, "user", generateTotpCode(main, device2)); } /* @@ -271,26 +285,34 @@ public void createDeviceAndVerifyCodeTest() throws Exception { public int triggerAndCheckRateLimit(Main main, TOTPDevice device) throws Exception { int N = Config.getConfig(main).getTotpMaxAttempts(); + // Sleep until we finish the current second so that TOTP verification won't change in the time limit + Thread.sleep(1000 - System.currentTimeMillis() % 1000 + 10); + Thread.sleep(1000); // sleep another second so that the rate limit state is kind of reset + // First N attempts should fail with invalid code: // This is to trigger rate limiting for (int i = 0; i < N; i++) { String code = "ic-" + i; // ic = invalid code + Thread.sleep(1); assertThrows( InvalidTotpException.class, - () -> Totp.verifyCode(main, "user", code, true)); + () -> Totp.verifyCode(main, "user", code)); } // Any kind of attempt after this should fail with rate limiting error. // This should happen until rate limiting cooldown happens: + Thread.sleep(1); assertThrows( LimitReachedException.class, - () -> Totp.verifyCode(main, "user", "icN+1", true)); + () -> Totp.verifyCode(main, "user", "icN+1")); + Thread.sleep(1); assertThrows( LimitReachedException.class, - () -> Totp.verifyCode(main, "user", generateTotpCode(main, device), true)); + () -> Totp.verifyCode(main, "user", generateTotpCode(main, device))); + Thread.sleep(1); assertThrows( LimitReachedException.class, - () -> Totp.verifyCode(main, "user", "icN+2", true)); + () -> Totp.verifyCode(main, "user", "icN+2")); return N; } @@ -312,12 +334,13 @@ public void rateLimitCooldownTest() throws Exception { } FeatureFlagTestContent.getInstance(process.main) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.TOTP}); + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MFA}); Main main = process.getProcess(); // Create device TOTPDevice device = Totp.registerDevice(main, "user", "deviceName", 1, 1); + Totp.verifyDevice(main, "user", device.deviceName, generateTotpCode(main, device, -1)); // Trigger rate limiting and fix it with a correct code after some time: int attemptsRequired = triggerAndCheckRateLimit(main, device); @@ -325,17 +348,17 @@ public void rateLimitCooldownTest() throws Exception { // Wait for 1 second (Should cool down rate limiting): Thread.sleep(1000); // But again try with invalid code: - assertThrows(InvalidTotpException.class, () -> Totp.verifyCode(main, "user", "invalid0", true)); + assertThrows(InvalidTotpException.class, () -> Totp.verifyCode(main, "user", "invalid0")); // This triggered rate limiting again. So even valid codes will fail for // another cooldown period: assertThrows(LimitReachedException.class, - () -> Totp.verifyCode(main, "user", generateTotpCode(main, device), true)); + () -> Totp.verifyCode(main, "user", generateTotpCode(main, device))); // Wait for 1 second (Should cool down rate limiting): Thread.sleep(1000); // Now try with valid code: - Totp.verifyCode(main, "user", generateTotpCode(main, device), true); + Totp.verifyCode(main, "user", generateTotpCode(main, device)); // Now invalid code shouldn't trigger rate limiting. Unless you do it N times: - assertThrows(InvalidTotpException.class, () -> Totp.verifyCode(main, "user", "invaldd", true)); + assertThrows(InvalidTotpException.class, () -> Totp.verifyCode(main, "user", "invaldd")); } @Test @@ -354,16 +377,16 @@ public void cronRemovesCodesDuringRateLimitTest() throws Exception { int attemptsRequired = triggerAndCheckRateLimit(main, device); assert attemptsRequired == 5; // Wait for 1 second so that all the codes expire: - Thread.sleep(1500); + Thread.sleep(1100); // Manually run cronjob to delete all the codes after their // expiry time + rate limiting period is over: DeleteExpiredTotpTokens.getInstance(main).run(); // This removal shouldn't affect rate limiting. User must remain rate limited. assertThrows(LimitReachedException.class, - () -> Totp.verifyCode(main, "user", generateTotpCode(main, device), true)); + () -> Totp.verifyCode(main, "user", generateTotpCode(main, device))); assertThrows(LimitReachedException.class, - () -> Totp.verifyCode(main, "user", "yet-ic", true)); + () -> Totp.verifyCode(main, "user", "yet-ic")); } @Test @@ -378,7 +401,7 @@ public void createAndVerifyDeviceTest() throws Exception { TOTPDevice device = Totp.registerDevice(main, "user", "deviceName", 1, 30); // Try verify non-existent user: - assertThrows(TotpNotEnabledException.class, + assertThrows(UnknownDeviceException.class, () -> Totp.verifyDevice(main, "non-existent-user", "deviceName", "XXXX")); // Try verify non-existent device @@ -388,7 +411,9 @@ public void createAndVerifyDeviceTest() throws Exception { // Verify device with wrong code assertThrows(InvalidTotpException.class, () -> Totp.verifyDevice(main, "user", "deviceName", "ic0")); + // Verify device with correct code + Thread.sleep(1); String validCode = generateTotpCode(main, device); boolean justVerfied = Totp.verifyDevice(main, "user", "deviceName", validCode); assert justVerfied; @@ -424,20 +449,30 @@ public void removeDeviceTest() throws Exception { TOTPDevice device1 = Totp.registerDevice(main, "user", "device1", 1, 30); TOTPDevice device2 = Totp.registerDevice(main, "user", "device2", 1, 30); + Thread.sleep(1); + Totp.verifyDevice(main, "user", "device1", generateTotpCode(main, device1, -1)); + Thread.sleep(1); + Totp.verifyDevice(main, "user", "device2", generateTotpCode(main, device2, -1)); + TOTPDevice[] devices = Totp.getDevices(main, "user"); assert (devices.length == 2); // Try to delete device for non-existent user: - assertThrows(TotpNotEnabledException.class, () -> Totp.removeDevice(main, "non-existent-user", "device1")); + assertThrows(UnknownDeviceException.class, () -> Totp.removeDevice(main, "non-existent-user", "device1")); // Try to delete non-existent device: assertThrows(UnknownDeviceException.class, () -> Totp.removeDevice(main, "user", "non-existent-device")); // Delete one of the devices { - assertThrows(InvalidTotpException.class, () -> Totp.verifyCode(main, "user", "ic0", true)); - Totp.verifyCode(main, "user", generateTotpCode(main, device1), true); - Totp.verifyCode(main, "user", generateTotpCode(main, device2), true); + assertThrows(InvalidTotpException.class, () -> Totp.verifyCode(main, "user", "ic0")); + + Thread.sleep(1000 - System.currentTimeMillis() % 1000 + 10); + + Thread.sleep(1); + Totp.verifyCode(main, "user", generateTotpCode(main, device1)); + Thread.sleep(1); + Totp.verifyCode(main, "user", generateTotpCode(main, device2)); // Delete device1 Totp.removeDevice(main, "user", "device1"); @@ -447,7 +482,7 @@ public void removeDeviceTest() throws Exception { // 1 device still remain so all codes should still be still there: TOTPUsedCode[] usedCodes = getAllUsedCodesUtil(storage, "user"); - assert (usedCodes.length == 3); + assert (usedCodes.length == 5); // 2 for device verification and 3 for code verification } // Deleting the last device of a user should delete all related codes: @@ -456,14 +491,16 @@ public void removeDeviceTest() throws Exception { // Create another user to test that other users aren't affected: TOTPDevice otherUserDevice = Totp.registerDevice(main, "other-user", "device", 1, 30); - Totp.verifyCode(main, "other-user", generateTotpCode(main, otherUserDevice), true); - assertThrows(InvalidTotpException.class, () -> Totp.verifyCode(main, "other-user", "ic1", true)); + Totp.verifyDevice(main, "other-user", "device", generateTotpCode(main, otherUserDevice, -1)); + Thread.sleep(1); + Totp.verifyCode(main, "other-user", generateTotpCode(main, otherUserDevice)); + assertThrows(InvalidTotpException.class, () -> Totp.verifyCode(main, "other-user", "ic1")); // Delete device2 Totp.removeDevice(main, "user", "device2"); - // TOTP has ben disabled for the user: - assertThrows(TotpNotEnabledException.class, () -> Totp.getDevices(main, "user")); + // No more devices are left for the user: + assert (Totp.getDevices(main, "user").length == 0); // No device left so all codes of the user should be deleted: TOTPUsedCode[] usedCodes = getAllUsedCodesUtil(storage, "user"); @@ -474,7 +511,7 @@ public void removeDeviceTest() throws Exception { assert (otherUserDevices.length == 1); usedCodes = getAllUsedCodesUtil(storage, "other-user"); - assert (usedCodes.length == 2); + assert (usedCodes.length == 3); // 1 for device verification and 2 for code verification } } @@ -490,7 +527,7 @@ public void updateDeviceNameTest() throws Exception { Totp.registerDevice(main, "user", "device2", 1, 30); // Try update non-existent user: - assertThrows(TotpNotEnabledException.class, + assertThrows(UnknownDeviceException.class, () -> Totp.updateDeviceName(main, "non-existent-user", "device1", "new-device-name")); // Try update non-existent device: @@ -526,7 +563,7 @@ public void getDevicesTest() throws Exception { Main main = result.process.getProcess(); // Try get devices for non-existent user: - assertThrows(TotpNotEnabledException.class, () -> Totp.getDevices(main, "non-existent-user")); + assert (Totp.getDevices(main, "non-existent-user").length == 0); TOTPDevice device1 = Totp.registerDevice(main, "user", "device1", 2, 30); TOTPDevice device2 = Totp.registerDevice(main, "user", "device2", 1, 10); @@ -548,4 +585,86 @@ public void deleteExpiredTokensCronIntervalTest() throws Exception { assert DeleteExpiredTotpTokens.getInstance(main).getIntervalTimeSeconds() == 60 * 60; } + @Test + public void testRegisterDeviceWithSameNameAsAnUnverifiedDevice() throws Exception { + TestSetupResult result = defaultInit(); + if (result == null) { + return; + } + Main main = result.process.getProcess(); + + Totp.registerDevice(main, "user", "device1", 1, 30); + Totp.registerDevice(main, "user", "device1", 1, 30); + } + + @Test + public void testCurrentAndMaxAttemptsInExceptions() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MFA}); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + TOTPDevice device = Totp.registerDevice(process.getProcess(), "userId", "deviceName", 1, 30); + try { + Totp.verifyDevice(process.getProcess(), "userId", "deviceName", "123456"); + fail(); + } catch (InvalidTotpException e) { + assertEquals(1, e.currentAttempts); + assertEquals(5, e.maxAttempts); + } + Thread.sleep(1); + try { + Totp.verifyDevice(process.getProcess(), "userId", "deviceName", "223456"); + fail(); + } catch (InvalidTotpException e) { + assertEquals(2, e.currentAttempts); + assertEquals(5, e.maxAttempts); + } + Thread.sleep(1); + + try { + Totp.verifyDevice(process.getProcess(), "userId", "deviceName", "323456"); + fail(); + } catch (InvalidTotpException e) { + assertEquals(3, e.currentAttempts); + assertEquals(5, e.maxAttempts); + } + Thread.sleep(1); + + try { + Totp.verifyDevice(process.getProcess(), "userId", "deviceName", "423456"); + fail(); + } catch (InvalidTotpException e) { + assertEquals(4, e.currentAttempts); + assertEquals(5, e.maxAttempts); + } + Thread.sleep(1); + + try { + Totp.verifyDevice(process.getProcess(), "userId", "deviceName", "523456"); + fail(); + } catch (InvalidTotpException e) { + assertEquals(5, e.currentAttempts); + assertEquals(5, e.maxAttempts); + } + Thread.sleep(1); + + try { + Totp.verifyDevice(process.getProcess(), "userId", "deviceName", "623456"); + fail(); + } catch (LimitReachedException e) { + assertEquals(5, e.currentAttempts); + assertEquals(5, e.maxAttempts); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/src/test/java/io/supertokens/test/totp/TOTPStorageTest.java b/src/test/java/io/supertokens/test/totp/TOTPStorageTest.java index ebd2bf133..8bb8f0936 100644 --- a/src/test/java/io/supertokens/test/totp/TOTPStorageTest.java +++ b/src/test/java/io/supertokens/test/totp/TOTPStorageTest.java @@ -14,8 +14,8 @@ import io.supertokens.pluginInterface.totp.TOTPStorage; import io.supertokens.pluginInterface.totp.TOTPUsedCode; import io.supertokens.pluginInterface.totp.exception.DeviceAlreadyExistsException; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; +import io.supertokens.pluginInterface.totp.exception.UnknownTotpUserIdException; import io.supertokens.pluginInterface.totp.exception.UsedCodeAlreadyExistsException; import io.supertokens.pluginInterface.totp.sqlStorage.TOTPSQLStorage; import io.supertokens.storageLayer.StorageLayer; @@ -27,8 +27,6 @@ import org.junit.Test; import org.junit.rules.TestRule; -import java.io.IOException; - import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; @@ -70,7 +68,7 @@ public TestSetupResult initSteps() TOTPSQLStorage storage = (TOTPSQLStorage) StorageLayer.getStorage(process.getProcess()); FeatureFlagTestContent.getInstance(process.main) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.TOTP}); + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MFA}); return new TestSetupResult(storage, process); } @@ -89,7 +87,7 @@ private static TOTPUsedCode[] getAllUsedCodesUtil(TOTPStorage storage, String us } public static void insertUsedCodesUtil(TOTPSQLStorage storage, TOTPUsedCode[] usedCodes) - throws StorageQueryException, StorageTransactionLogicException, TotpNotEnabledException, + throws StorageQueryException, StorageTransactionLogicException, UnknownDeviceException, UsedCodeAlreadyExistsException { try { storage.startTransaction(con -> { @@ -97,7 +95,7 @@ public static void insertUsedCodesUtil(TOTPSQLStorage storage, TOTPUsedCode[] us for (TOTPUsedCode usedCode : usedCodes) { storage.insertUsedCode_Transaction(con, new TenantIdentifier(null, null, null), usedCode); } - } catch (TotpNotEnabledException | UsedCodeAlreadyExistsException e) { + } catch (UnknownTotpUserIdException | UsedCodeAlreadyExistsException e) { throw new StorageTransactionLogicException(e); } catch (TenantOrAppNotFoundException e) { throw new IllegalStateException(e); @@ -108,8 +106,8 @@ public static void insertUsedCodesUtil(TOTPSQLStorage storage, TOTPUsedCode[] us }); } catch (StorageTransactionLogicException e) { Exception actual = e.actualException; - if (actual instanceof TotpNotEnabledException) { - throw (TotpNotEnabledException) actual; + if (actual instanceof UnknownDeviceException) { + throw (UnknownDeviceException) actual; } else if (actual instanceof UsedCodeAlreadyExistsException) { throw (UsedCodeAlreadyExistsException) actual; } @@ -125,9 +123,9 @@ public void createDeviceTests() throws Exception { } TOTPSQLStorage storage = result.storage; - TOTPDevice device1 = new TOTPDevice("user", "d1", "secret", 30, 1, false); - TOTPDevice device2 = new TOTPDevice("user", "d2", "secret", 30, 1, true); - TOTPDevice device2Duplicate = new TOTPDevice("user", "d2", "new-secret", 30, 1, false); + TOTPDevice device1 = new TOTPDevice("user", "d1", "secret", 30, 1, false, System.currentTimeMillis()); + TOTPDevice device2 = new TOTPDevice("user", "d2", "secret", 30, 1, true, System.currentTimeMillis()); + TOTPDevice device2Duplicate = new TOTPDevice("user", "d2", "new-secret", 30, 1, false, System.currentTimeMillis()); storage.createDevice(new AppIdentifier(null, null), device1); @@ -157,7 +155,7 @@ public void verifyDeviceTests() throws Exception { } TOTPSQLStorage storage = result.storage; - TOTPDevice device = new TOTPDevice("user", "device", "secretKey", 30, 1, false); + TOTPDevice device = new TOTPDevice("user", "device", "secretKey", 30, 1, false, System.currentTimeMillis()); storage.createDevice(new AppIdentifier(null, null), device); TOTPDevice[] storedDevices = storage.getDevices(new AppIdentifier(null, null), "user"); @@ -197,8 +195,8 @@ public void getDevicesCount_TransactionTests() throws Exception { }); assert devicesCount == 0; - TOTPDevice device1 = new TOTPDevice("user", "device1", "sk1", 30, 1, false); - TOTPDevice device2 = new TOTPDevice("user", "device2", "sk2", 30, 1, false); + TOTPDevice device1 = new TOTPDevice("user", "device1", "sk1", 30, 1, false, System.currentTimeMillis()); + TOTPDevice device2 = new TOTPDevice("user", "device2", "sk2", 30, 1, false, System.currentTimeMillis()); storage.createDevice(new AppIdentifier(null, null), device1); storage.createDevice(new AppIdentifier(null, null), device2); @@ -227,8 +225,8 @@ public void removeUser_TransactionTests() throws Exception { return null; }); - TOTPDevice device1 = new TOTPDevice("user", "device1", "sk1", 30, 1, false); - TOTPDevice device2 = new TOTPDevice("user", "device2", "sk2", 30, 1, false); + TOTPDevice device1 = new TOTPDevice("user", "device1", "sk1", 30, 1, false, System.currentTimeMillis()); + TOTPDevice device2 = new TOTPDevice("user", "device2", "sk2", 30, 1, false, System.currentTimeMillis()); storage.createDevice(new AppIdentifier(null, null), device1); storage.createDevice(new AppIdentifier(null, null), device2); @@ -268,8 +266,8 @@ public void deleteDevice_TransactionTests() throws Exception { } TOTPSQLStorage storage = result.storage; - TOTPDevice device1 = new TOTPDevice("user", "device1", "sk1", 30, 1, false); - TOTPDevice device2 = new TOTPDevice("user", "device2", "sk2", 30, 1, false); + TOTPDevice device1 = new TOTPDevice("user", "device1", "sk1", 30, 1, false, System.currentTimeMillis()); + TOTPDevice device2 = new TOTPDevice("user", "device2", "sk2", 30, 1, false, System.currentTimeMillis()); storage.createDevice(new AppIdentifier(null, null), device1); storage.createDevice(new AppIdentifier(null, null), device2); @@ -316,7 +314,7 @@ public void updateDeviceNameTests() throws Exception { } TOTPSQLStorage storage = result.storage; - TOTPDevice device = new TOTPDevice("user", "device", "secretKey", 30, 1, false); + TOTPDevice device = new TOTPDevice("user", "device", "secretKey", 30, 1, false, System.currentTimeMillis()); storage.createDevice(new AppIdentifier(null, null), device); TOTPDevice[] storedDevices = storage.getDevices(new AppIdentifier(null, null), "user"); @@ -337,7 +335,7 @@ public void updateDeviceNameTests() throws Exception { // Try to create a new device and rename it to the same name as an existing // device: - TOTPDevice newDevice = new TOTPDevice("user", "new-device", "secretKey", 30, 1, false); + TOTPDevice newDevice = new TOTPDevice("user", "new-device", "secretKey", 30, 1, false, System.currentTimeMillis()); storage.createDevice(new AppIdentifier(null, null), newDevice); assertThrows(DeviceAlreadyExistsException.class, @@ -356,8 +354,8 @@ public void getDevicesTest() throws Exception { } TOTPSQLStorage storage = result.storage; - TOTPDevice device1 = new TOTPDevice("user", "d1", "secretKey", 30, 1, false); - TOTPDevice device2 = new TOTPDevice("user", "d2", "secretKey", 30, 1, false); + TOTPDevice device1 = new TOTPDevice("user", "d1", "secretKey", 30, 1, false, System.currentTimeMillis()); + TOTPDevice device2 = new TOTPDevice("user", "d2", "secretKey", 30, 1, false, System.currentTimeMillis()); storage.createDevice(new AppIdentifier(null, null), device1); storage.createDevice(new AppIdentifier(null, null), device2); @@ -384,7 +382,7 @@ public void insertUsedCodeTest() throws Exception { // Insert a long lasting valid code and check that it's returned when queried: { - TOTPDevice device = new TOTPDevice("user", "device", "secretKey", 30, 1, false); + TOTPDevice device = new TOTPDevice("user", "device", "secretKey", 30, 1, false, System.currentTimeMillis()); TOTPUsedCode code = new TOTPUsedCode("user", "1234", true, nextDay, now); storage.createDevice(new AppIdentifier(null, null), device); @@ -404,16 +402,18 @@ public void insertUsedCodeTest() throws Exception { // Try to insert code when user doesn't have any device (i.e. TOTP not enabled) { - assertThrows(TotpNotEnabledException.class, + StorageTransactionLogicException e = assertThrows(StorageTransactionLogicException.class, () -> insertUsedCodesUtil(storage, new TOTPUsedCode[]{ new TOTPUsedCode("new-user-without-totp", "1234", true, nextDay, System.currentTimeMillis()) })); + + // assert e.actualException instanceof UnknownDeviceException } // Try to insert code after user has atleast one device (i.e. TOTP enabled) { - TOTPDevice newDevice = new TOTPDevice("user", "new-device", "secretKey", 30, 1, false); + TOTPDevice newDevice = new TOTPDevice("user", "new-device", "secretKey", 30, 1, false, System.currentTimeMillis()); storage.createDevice(new AppIdentifier(null, null), newDevice); insertUsedCodesUtil( storage, @@ -423,11 +423,13 @@ public void insertUsedCodeTest() throws Exception { } // Try to insert code when user doesn't exist: - assertThrows(TotpNotEnabledException.class, + StorageTransactionLogicException e = assertThrows(StorageTransactionLogicException.class, () -> insertUsedCodesUtil(storage, new TOTPUsedCode[]{ new TOTPUsedCode("non-existent-user", "1234", true, nextDay, System.currentTimeMillis()) })); + + // assert e.actualException instanceof UnknownDeviceException; } @Test @@ -445,7 +447,7 @@ public void getAllUsedCodesTest() throws Exception { long nextDay = now + 1000 * 60 * 60 * 24; // 1 day from now long prevDay = now - 1000 * 60 * 60 * 24; // 1 day ago - TOTPDevice device = new TOTPDevice("user", "device", "secretKey", 30, 1, false); + TOTPDevice device = new TOTPDevice("user", "device", "secretKey", 30, 1, false, System.currentTimeMillis()); TOTPUsedCode validCode1 = new TOTPUsedCode("user", "valid1", true, nextDay, now + 1); TOTPUsedCode invalidCode = new TOTPUsedCode("user", "invalid", false, nextDay, now + 2); TOTPUsedCode expiredCode = new TOTPUsedCode("user", "expired", true, prevDay, now + 3); @@ -491,7 +493,7 @@ public void removeExpiredCodesTest() throws Exception { long nextDay = System.currentTimeMillis() + 1000 * 60 * 60 * 24; // 1 day from now long hundredMs = System.currentTimeMillis() + 100; // 100ms from now - TOTPDevice device = new TOTPDevice("user", "device", "secretKey", 30, 1, false); + TOTPDevice device = new TOTPDevice("user", "device", "secretKey", 30, 1, false, System.currentTimeMillis()); TOTPUsedCode validCodeToLive = new TOTPUsedCode("user", "valid", true, nextDay, now); TOTPUsedCode invalidCodeToLive = new TOTPUsedCode("user", "invalid", false, nextDay, now + 1); TOTPUsedCode validCodeToExpire = new TOTPUsedCode("user", "valid", true, hundredMs, now + 2); diff --git a/src/test/java/io/supertokens/test/totp/TotpLicenseTest.java b/src/test/java/io/supertokens/test/totp/TotpLicenseTest.java index a46ca392e..9dfda6763 100644 --- a/src/test/java/io/supertokens/test/totp/TotpLicenseTest.java +++ b/src/test/java/io/supertokens/test/totp/TotpLicenseTest.java @@ -42,8 +42,7 @@ import static org.junit.Assert.assertThrows; public class TotpLicenseTest { - public final static String OPAQUE_KEY_WITH_TOTP_FEATURE = "pXhNK=nYiEsb6gJEOYP2kIR6M0kn4XLvNqcwT1XbX8xHtm44K" + - "-lQfGCbaeN0Ieeza39fxkXr=tiiUU=DXxDH40Y=4FLT4CE-rG1ETjkXxO4yucLpJvw3uSegPayoISGL"; + public final static String OPAQUE_KEY_WITH_MFA_FEATURE = "Qk8olVa=v-9PU=snnUFMF4ihMCx4zVBOO6Jd7Nrg6Cg5YyFliEj252ADgpwEpDLfFowA0U5OyVo3XL=U4FMft2HDHCDGg9hWD4iwQQiyjMRi6Mu03CVbAxIkNGaXtJ53"; @Rule public TestRule watchman = Utils.getOnFailure(); @@ -99,7 +98,7 @@ public void testTotpWithoutLicense() throws Exception { }); // Verify code assertThrows(FeatureNotEnabledException.class, () -> { - Totp.verifyCode(main, "user", "device1", true); + Totp.verifyCode(main, "user", "device1"); }); // Try to create device via API: @@ -126,14 +125,13 @@ public void testTotpWithoutLicense() throws Exception { } ); assert e.statusCode == 402; - assert e.getMessage().contains("TOTP feature is not enabled"); + assert e.getMessage().contains("MFA feature is not enabled"); // Try to verify code via API: JsonObject body2 = new JsonObject(); body2.addProperty("userId", "user-id"); body2.addProperty("totp", "123456"); - body2.addProperty("allowUnverifiedDevices", true); HttpResponseException e2 = assertThrows( @@ -152,7 +150,7 @@ public void testTotpWithoutLicense() throws Exception { } ); assert e2.statusCode == 402; - assert e2.getMessage().contains("TOTP feature is not enabled"); + assert e2.getMessage().contains("MFA feature is not enabled"); } @@ -163,15 +161,19 @@ public void testTotpWithLicense() throws Exception { return; } FeatureFlagTestContent.getInstance(result.process.main) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.TOTP}); + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MFA}); Main main = result.process.getProcess(); // Create device TOTPDevice device = Totp.registerDevice(main, "user", "device1", 1, 30); + // Verify device + String code = generateTotpCode(main, device, 0); + Totp.verifyDevice(main, device.userId, device.deviceName, code); // Verify code - String code = generateTotpCode(main, device); - Totp.verifyCode(main, "user", code, true); + Thread.sleep(1); + String nextCode = generateTotpCode(main, device, 1); + Totp.verifyCode(main, "user", nextCode); } diff --git a/src/test/java/io/supertokens/test/totp/api/CreateTotpDeviceAPITest.java b/src/test/java/io/supertokens/test/totp/api/CreateTotpDeviceAPITest.java index 3c181fbaa..99d6ecbdb 100644 --- a/src/test/java/io/supertokens/test/totp/api/CreateTotpDeviceAPITest.java +++ b/src/test/java/io/supertokens/test/totp/api/CreateTotpDeviceAPITest.java @@ -6,12 +6,15 @@ import io.supertokens.featureflag.FeatureFlag; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.totp.TOTPDevice; 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.test.totp.TOTPRecipeTest; import io.supertokens.test.totp.TotpLicenseTest; +import io.supertokens.totp.Totp; import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; @@ -78,9 +81,9 @@ public void testApi() throws Exception { } FeatureFlag.getInstance(process.main) - .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_TOTP_FEATURE); + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); FeatureFlagTestContent.getInstance(process.main) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.TOTP}); + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MFA}); if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; @@ -88,17 +91,15 @@ public void testApi() throws Exception { JsonObject body = new JsonObject(); - // Missing userId/deviceName/skew/period + // Missing userId/skew/period { Exception e = createDeviceRequest(process, body); checkFieldMissingErrorResponse(e, "userId"); + + body.addProperty("deviceName", ""); body.addProperty("userId", ""); e = createDeviceRequest(process, body); - checkFieldMissingErrorResponse(e, "deviceName"); - - body.addProperty("deviceName", ""); - e = createDeviceRequest(process, body); checkFieldMissingErrorResponse(e, "skew"); body.addProperty("skew", -1); @@ -138,8 +139,10 @@ public void testApi() throws Exception { Utils.getCdiVersionStringLatestForTests(), "totp"); assert res.get("status").getAsString().equals("OK"); + assert res.get("deviceName").getAsString().equals("d1"); - // try again with same device: + // try again with same device name: + // This should replace the previous device JsonObject res2 = HttpRequestForTesting.sendJsonPOSTRequest( process.getProcess(), "", @@ -150,7 +153,103 @@ public void testApi() throws Exception { null, Utils.getCdiVersionStringLatestForTests(), "totp"); + assert res2.get("status").getAsString().equals("OK"); + assert res.get("deviceName").getAsString().equals("d1"); + + // verify d1 + { + TOTPDevice device = Totp.getDevices(process.getProcess(), "user-id" )[0]; + String validTotp = TOTPRecipeTest.generateTotpCode(process.getProcess(), device); + Totp.verifyDevice(process.getProcess(), "user-id", "d1", validTotp); + } + + // try again with same device name: + res2 = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); assert res2.get("status").getAsString().equals("DEVICE_ALREADY_EXISTS_ERROR"); + assert res.get("deviceName").getAsString().equals("d1"); + + // try without passing deviceName: + body.remove("deviceName"); + JsonObject res3 = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res3.get("status").getAsString().equals("OK"); + assert res3.get("deviceName").getAsString().equals("TOTP Device 1"); + String attempt1Secret = res3.get("secret").getAsString(); + + // try again without passing deviceName: + // should re-create the device since "TOTP Device 1" wasn't verified + JsonObject res4 = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res4.get("status").getAsString().equals("OK"); + assert res3.get("deviceName").getAsString().equals("TOTP Device 1"); + String attempt2Secret = res4.get("secret").getAsString(); + assert !attempt1Secret.equals(attempt2Secret); + + // verify the device: + TOTPDevice device = new TOTPDevice( + "user-id", + "TOTP Device 1", + attempt2Secret, + 30, + 0, + false, + System.currentTimeMillis() + ); + JsonObject verifyDeviceBody = new JsonObject(); + verifyDeviceBody.addProperty("userId", device.userId); + verifyDeviceBody.addProperty("deviceName", device.deviceName); + verifyDeviceBody.addProperty("totp", TOTPRecipeTest.generateTotpCode(process.getProcess(), device)); + JsonObject res5 = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device/verify", + verifyDeviceBody, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res5.get("status").getAsString().equals("OK"); + + // now try to create a device: + // "TOTP Device 1" has been verified, it won't replace it + JsonObject res6 = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res6.get("status").getAsString().equals("OK"); + assert res6.get("deviceName").getAsString().equals("TOTP Device 2"); } process.kill(); diff --git a/src/test/java/io/supertokens/test/totp/api/GetTotpDevicesAPITest.java b/src/test/java/io/supertokens/test/totp/api/GetTotpDevicesAPITest.java index 5450be943..e48ba2b29 100644 --- a/src/test/java/io/supertokens/test/totp/api/GetTotpDevicesAPITest.java +++ b/src/test/java/io/supertokens/test/totp/api/GetTotpDevicesAPITest.java @@ -77,7 +77,7 @@ public void testApi() throws Exception { TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - FeatureFlagTestContent.getInstance(process.main).setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.TOTP }); + FeatureFlagTestContent.getInstance(process.main).setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.MFA }); if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; @@ -153,7 +153,8 @@ public void testApi() throws Exception { null, Utils.getCdiVersionStringLatestForTests(), "totp"); - assert res2.get("status").getAsString().equals("TOTP_NOT_ENABLED_ERROR"); + assert res2.get("status").getAsString().equals("OK"); + assert res2.get("devices").getAsJsonArray().size() == 0; } process.kill(); diff --git a/src/test/java/io/supertokens/test/totp/api/ImportTotpDeviceAPITest.java b/src/test/java/io/supertokens/test/totp/api/ImportTotpDeviceAPITest.java new file mode 100644 index 000000000..8ac5a3c2d --- /dev/null +++ b/src/test/java/io/supertokens/test/totp/api/ImportTotpDeviceAPITest.java @@ -0,0 +1,261 @@ +package io.supertokens.test.totp.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlag; +import io.supertokens.featureflag.FeatureFlagTestContent; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.totp.TOTPDevice; +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.test.totp.TOTPRecipeTest; +import io.supertokens.test.totp.TotpLicenseTest; +import io.supertokens.totp.Totp; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static io.supertokens.test.totp.TOTPRecipeTest.generateTotpCode; +import static org.junit.Assert.*; + +public class ImportTotpDeviceAPITest { + + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + private Exception importDeviceRequest(TestingProcessManager.TestingProcess process, JsonObject body) { + return assertThrows( + HttpResponseException.class, + () -> HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device/import", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp")); + } + + private void checkFieldMissingErrorResponse(Exception ex, String fieldName) { + assert ex instanceof HttpResponseException; + HttpResponseException e = (HttpResponseException) ex; + assert e.statusCode == 400; + assertTrue(e.getMessage().contains( + "Http error. Status Code: 400. Message: Field name '" + fieldName + "' is invalid in JSON input")); + } + + private void checkResponseErrorContains(Exception ex, String msg) { + assert ex instanceof HttpResponseException; + HttpResponseException e = (HttpResponseException) ex; + assert e.statusCode == 400; + assertTrue(e.getMessage().contains(msg)); + } + + @Test + public void testApi() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MFA}); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + String secret = "ZNPARPDTO6BFVSOFM3BPJGORPYTNTDSF"; + + JsonObject body = new JsonObject(); + + // Missing userId/skew/period + { + Exception e = importDeviceRequest(process, body); + checkFieldMissingErrorResponse(e, "userId"); + + body.addProperty("deviceName", ""); + + body.addProperty("userId", ""); + e = importDeviceRequest(process, body); + checkFieldMissingErrorResponse(e, "skew"); + + body.addProperty("skew", -1); + e = importDeviceRequest(process, body); + checkFieldMissingErrorResponse(e, "period"); + + body.addProperty("period", 0); + e = importDeviceRequest(process, body); + checkFieldMissingErrorResponse(e, "secretKey"); + + } + + // Invalid userId/deviceName/skew/period + { + body.addProperty("secretKey", ""); + Exception e = importDeviceRequest(process, body); + checkResponseErrorContains(e, "userId cannot be empty"); // Note that this is not a field missing error + + body.addProperty("userId", "user-id"); + e = importDeviceRequest(process, body); + checkResponseErrorContains(e, "deviceName cannot be empty"); + + body.addProperty("deviceName", "d1"); + e = importDeviceRequest(process, body); + checkResponseErrorContains(e, "secretKey cannot be empty"); + + body.addProperty("secretKey", secret); + e = importDeviceRequest(process, body); + checkResponseErrorContains(e, "skew must be >= 0"); + + body.addProperty("skew", 0); + e = importDeviceRequest(process, body); + checkResponseErrorContains(e, "period must be > 0"); + + body.addProperty("period", 30); + + // should pass now: + JsonObject res = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device/import", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res.get("status").getAsString().equals("OK"); + assertEquals("d1", res.get("deviceName").getAsString()); + + // try again with same device name: + JsonObject res2 = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device/import", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res2.get("status").getAsString().equals("DEVICE_ALREADY_EXISTS_ERROR"); + } + + // Verify totp on the imported device + TOTPDevice device = new TOTPDevice("user-id", "d1", secret, 30, 0, false, System.currentTimeMillis()); + + JsonObject verifyDeviceReq = new JsonObject(); + verifyDeviceReq.addProperty("userId", device.userId); + verifyDeviceReq.addProperty("deviceName", device.deviceName); + verifyDeviceReq.addProperty("totp", generateTotpCode(process.getProcess(), device)); + + JsonObject verifyDeviceRes = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device/verify", + verifyDeviceReq, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assertEquals(verifyDeviceRes.get("status").getAsString(), "OK"); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testApiWithoutDeviceName() 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; + } + + FeatureFlag.getInstance(process.main) + .setLicenseKeyAndSyncFeatures(TotpLicenseTest.OPAQUE_KEY_WITH_MFA_FEATURE); + FeatureFlagTestContent.getInstance(process.main) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MFA}); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + String secret = "ZNPARPDTO6BFVSOFM3BPJGORPYTNTDSF"; + + JsonObject body = new JsonObject(); + body.addProperty("secretKey", ""); + body.addProperty("userId", "user-id"); + body.addProperty("secretKey", secret); + body.addProperty("skew", 0); + body.addProperty("period", 30); + + JsonObject res = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device/import", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res.get("status").getAsString().equals("OK"); + assertEquals("TOTP Device 0", res.get("deviceName").getAsString()); + } + + { // Check for device already exists + String secret = "ZNPARPDTO6BFVSOFM3BPJGORPYTNTDSF"; + + JsonObject body = new JsonObject(); + body.addProperty("secretKey", ""); + body.addProperty("userId", "user-id"); + body.addProperty("secretKey", secret); + body.addProperty("skew", 0); + body.addProperty("period", 30); + body.addProperty("deviceName", "TOTP Device 0"); + + JsonObject res = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device/import", + body, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assert res.get("status").getAsString().equals("DEVICE_ALREADY_EXISTS_ERROR"); + } + } +} diff --git a/src/test/java/io/supertokens/test/totp/api/MultitenantAPITest.java b/src/test/java/io/supertokens/test/totp/api/MultitenantAPITest.java index fb3434cd4..85e2df589 100644 --- a/src/test/java/io/supertokens/test/totp/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/totp/api/MultitenantAPITest.java @@ -39,6 +39,7 @@ import io.supertokens.test.httpRequest.HttpResponseException; import io.supertokens.test.totp.TOTPRecipeTest; import io.supertokens.thirdparty.InvalidProviderConfigException; +import io.supertokens.totp.Totp; import io.supertokens.utils.SemVer; import org.junit.After; import org.junit.AfterClass; @@ -75,7 +76,7 @@ public void beforeEach() throws InterruptedException, InvalidProviderConfigExcep this.process = TestingProcessManager.start(args); FeatureFlagTestContent.getInstance(process.getProcess()) .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ - EE_FEATURES.MULTI_TENANCY, EE_FEATURES.TOTP}); + EE_FEATURES.MULTI_TENANCY, EE_FEATURES.MFA}); process.startProcess(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); @@ -108,6 +109,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -128,6 +130,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -148,6 +151,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -160,6 +164,11 @@ private void createTenants() private JsonObject createDevice(TenantIdentifier tenantIdentifier, String userId) throws HttpResponseException, IOException { + return createDevice(tenantIdentifier, userId, SemVer.v3_0); + } + + private JsonObject createDevice(TenantIdentifier tenantIdentifier, String userId, SemVer version) + throws HttpResponseException, IOException { JsonObject body = new JsonObject(); body.addProperty("userId", userId); body.addProperty("deviceName", "d1"); @@ -174,7 +183,7 @@ private JsonObject createDevice(TenantIdentifier tenantIdentifier, String userId 1000, 1000, null, - SemVer.v3_0.get(), + version.get(), "totp"); assertEquals("OK", res.get("status").getAsString()); return res; @@ -193,8 +202,8 @@ private void createDeviceAlreadyExists(TenantIdentifier tenantIdentifier, String "", HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/totp/device"), body, - 1000, - 1000, + 1000000, + 1000000, null, SemVer.v3_0.get(), "totp"); @@ -241,7 +250,7 @@ private void validateTotp(TenantIdentifier tenantIdentifier, String userId, Stri } @Test - public void testCreateDeviceWorksFromPublicTenantOnly() throws Exception { + public void testCreateDeviceWorksFromAllTenants() throws Exception { if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; } @@ -251,12 +260,35 @@ public void testCreateDeviceWorksFromPublicTenantOnly() throws Exception { TenantIdentifier[] tenants = new TenantIdentifier[]{t1, t2, t3}; for (TenantIdentifier tenantId : tenants) { createDevice(tenantId, "user"+userCount); - createDeviceAlreadyExists(tenantId, "user"+userCount); userCount++; } } + @Test + public void testCreateDeviceWorksFromPublicTenantOnly_v5() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + int userCount = 1; + + createDevice(t1, "user" + userCount); + TOTPDevice device = Totp.getDevices(t1.toAppIdentifier(), (StorageLayer.getStorage(t1, process.getProcess())), + "user" + userCount)[0]; + String validTotp = TOTPRecipeTest.generateTotpCode(process.getProcess(), device); + verifyDevice(t1, "user" + userCount, validTotp); + + userCount++; + + try { + createDevice(t2, "user" + userCount, SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } + } + @Test public void testSameCodeUsedOnDifferentTenantsIsAllowed() throws Exception { if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { @@ -273,7 +305,7 @@ public void testSameCodeUsedOnDifferentTenantsIsAllowed() throws Exception { JsonObject deviceResponse = createDevice(t1, userId); String secretKey = deviceResponse.get("secret").getAsString(); - TOTPDevice device = new TOTPDevice(userId, "d1", secretKey, 2, 1, true); + TOTPDevice device = new TOTPDevice("user" + userCount, "d1", secretKey, 2, 1, true, System.currentTimeMillis()); String validTotp = TOTPRecipeTest.generateTotpCode(process.getProcess(), device); verifyDevice(tenant1, userId, validTotp); diff --git a/src/test/java/io/supertokens/test/totp/api/RemoveTotpDeviceAPITest.java b/src/test/java/io/supertokens/test/totp/api/RemoveTotpDeviceAPITest.java index 845b42852..aa92e4ad0 100644 --- a/src/test/java/io/supertokens/test/totp/api/RemoveTotpDeviceAPITest.java +++ b/src/test/java/io/supertokens/test/totp/api/RemoveTotpDeviceAPITest.java @@ -78,7 +78,7 @@ public void testApi() throws Exception { } FeatureFlagTestContent.getInstance(process.main) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.TOTP}); + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MFA}); // Setup user and devices: JsonObject createDeviceReq = new JsonObject(); @@ -180,7 +180,8 @@ public void testApi() throws Exception { null, Utils.getCdiVersionStringLatestForTests(), "totp"); - assert res3.get("status").getAsString().equals("TOTP_NOT_ENABLED_ERROR"); + assert res3.get("status").getAsString().equals("OK"); + assert res3.get("didDeviceExist").getAsBoolean() == false; } process.kill(); 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 43c9f74d5..cdb7d23ae 100644 --- a/src/test/java/io/supertokens/test/totp/api/TotpUserIdMappingTest.java +++ b/src/test/java/io/supertokens/test/totp/api/TotpUserIdMappingTest.java @@ -48,7 +48,7 @@ public void testExternalUserIdTranslation() throws Exception { return; } - FeatureFlagTestContent.getInstance(process.main).setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.TOTP }); + FeatureFlagTestContent.getInstance(process.main).setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.MFA }); JsonObject body = new JsonObject(); @@ -61,7 +61,7 @@ public void testExternalUserIdTranslation() throws Exception { body.addProperty("userId", externalUserId); body.addProperty("deviceName", "d1"); - body.addProperty("skew", 0); + body.addProperty("skew", 1); body.addProperty("period", 30); // Register 1st device @@ -77,7 +77,7 @@ public void testExternalUserIdTranslation() throws Exception { "totp"); assert res1.get("status").getAsString().equals("OK"); String d1Secret = res1.get("secret").getAsString(); - TOTPDevice device1 = new TOTPDevice(externalUserId, "d1", d1Secret, 30, 0, false); + TOTPDevice device1 = new TOTPDevice(externalUserId, "d1", d1Secret, 30, 0, false, System.currentTimeMillis()); body.addProperty("deviceName", "d2"); @@ -93,14 +93,14 @@ public void testExternalUserIdTranslation() throws Exception { "totp"); assert res2.get("status").getAsString().equals("OK"); String d2Secret = res2.get("secret").getAsString(); - TOTPDevice device2 = new TOTPDevice(externalUserId, "d2", d2Secret, 30, 0, false); + TOTPDevice device2 = new TOTPDevice(externalUserId, "d2", d2Secret, 30, 0, false, System.currentTimeMillis()); // Verify d1 but not d2: JsonObject verifyD1Input = new JsonObject(); verifyD1Input.addProperty("userId", externalUserId); - String d1Totp = TOTPRecipeTest.generateTotpCode(process.getProcess(), device1); + String d1VerifyTotp = TOTPRecipeTest.generateTotpCode(process.getProcess(), device1); verifyD1Input.addProperty("deviceName", "d1"); - verifyD1Input.addProperty("totp", d1Totp ); + verifyD1Input.addProperty("totp", d1VerifyTotp); JsonObject verifyD1Res = HttpRequestForTesting.sendJsonPOSTRequest( process.getProcess(), @@ -116,25 +116,43 @@ public void testExternalUserIdTranslation() throws Exception { assert verifyD1Res.get("status").getAsString().equals("OK"); assert verifyD1Res.get("wasAlreadyVerified").getAsBoolean() == false; - // use d2 to login in totp: - JsonObject loginInput = new JsonObject(); - loginInput.addProperty("userId", externalUserId); - String d2Totp = TOTPRecipeTest.generateTotpCode(process.getProcess(), device2); - loginInput.addProperty("totp", d2Totp); // use code from d2 which is unverified - loginInput.addProperty("allowUnverifiedDevices", true); + // use d2 to login in totp: (should fail coz it's not verified) + JsonObject d2LoginInput = new JsonObject(); + d2LoginInput.addProperty("userId", externalUserId); + String d2Totp = TOTPRecipeTest.generateTotpCode(process.getProcess(), device2, 1); + d2LoginInput.addProperty("totp", d2Totp); // use code from d2 which is unverified - JsonObject loginRes = HttpRequestForTesting.sendJsonPOSTRequest( + JsonObject d2LoginRes = HttpRequestForTesting.sendJsonPOSTRequest( process.getProcess(), "", "http://localhost:3567/recipe/totp/verify", - loginInput, + d2LoginInput, 1000, 1000, null, Utils.getCdiVersionStringLatestForTests(), "totp"); - assert loginRes.get("status").getAsString().equals("OK"); + assert d2LoginRes.get("status").getAsString().equals("INVALID_TOTP_ERROR"); + + // use d1 to login in totp: (should pass) + JsonObject d1LoginInput = new JsonObject(); + d1LoginInput.addProperty("userId", externalUserId); + String d1Totp = TOTPRecipeTest.generateTotpCode(process.getProcess(), device1, 1); + d1LoginInput.addProperty("totp", d1Totp); // use code from d2 which is unverified + + JsonObject d1LoginRes = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/verify", + d1LoginInput, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + + assert d1LoginRes.get("status").getAsString().equals("OK"); // Change the name of d1 to d3: JsonObject updateDeviceNameInput = new JsonObject(); diff --git a/src/test/java/io/supertokens/test/totp/api/UpdateTotpDeviceAPITest.java b/src/test/java/io/supertokens/test/totp/api/UpdateTotpDeviceAPITest.java index c94d72ff0..27c5f9fea 100644 --- a/src/test/java/io/supertokens/test/totp/api/UpdateTotpDeviceAPITest.java +++ b/src/test/java/io/supertokens/test/totp/api/UpdateTotpDeviceAPITest.java @@ -76,7 +76,7 @@ public void testApi() throws Exception { return; } - FeatureFlagTestContent.getInstance(process.main).setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.TOTP }); + FeatureFlagTestContent.getInstance(process.main).setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.MFA }); // Setup user and devices: JsonObject createDeviceReq = new JsonObject(); @@ -199,7 +199,7 @@ public void testApi() throws Exception { null, Utils.getCdiVersionStringLatestForTests(), "totp"); - assert res4.get("status").getAsString().equals("TOTP_NOT_ENABLED_ERROR"); + assert res4.get("status").getAsString().equals("UNKNOWN_DEVICE_ERROR"); } process.kill(); diff --git a/src/test/java/io/supertokens/test/totp/api/VerifyTotpAPITest.java b/src/test/java/io/supertokens/test/totp/api/VerifyTotpAPITest.java index 57d765a51..31018ba57 100644 --- a/src/test/java/io/supertokens/test/totp/api/VerifyTotpAPITest.java +++ b/src/test/java/io/supertokens/test/totp/api/VerifyTotpAPITest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.junit.rules.TestRule; +import static io.supertokens.test.totp.TOTPRecipeTest.generateTotpCode; import static org.junit.Assert.*; public class VerifyTotpAPITest { @@ -40,7 +41,7 @@ public void beforeEach() { Utils.reset(); } - private Exception updateDeviceRequest(TestingProcessManager.TestingProcess process, JsonObject body) { + private Exception verifyTotpCodeRequest(TestingProcessManager.TestingProcess process, JsonObject body) { return assertThrows( io.supertokens.test.httpRequest.HttpResponseException.class, () -> HttpRequestForTesting.sendJsonPOSTRequest( @@ -87,13 +88,13 @@ public void testApi() throws Exception { } FeatureFlagTestContent.getInstance(process.main) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.TOTP}); + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MFA}); // Setup user and devices: JsonObject createDeviceReq = new JsonObject(); createDeviceReq.addProperty("userId", "user-id"); createDeviceReq.addProperty("deviceName", "deviceName"); - createDeviceReq.addProperty("period", 30); + createDeviceReq.addProperty("period", 2); createDeviceReq.addProperty("skew", 0); JsonObject createDeviceRes = HttpRequestForTesting.sendJsonPOSTRequest( @@ -109,44 +110,56 @@ public void testApi() throws Exception { assertEquals(createDeviceRes.get("status").getAsString(), "OK"); String secretKey = createDeviceRes.get("secret").getAsString(); - TOTPDevice device = new TOTPDevice("user-id", "deviceName", secretKey, 30, 0, false); + TOTPDevice device = new TOTPDevice("user-id", "deviceName", secretKey, 2, 0, false, System.currentTimeMillis()); - // Start the actual tests for update device API: + JsonObject verifyDeviceReq = new JsonObject(); + verifyDeviceReq.addProperty("userId", device.userId); + verifyDeviceReq.addProperty("deviceName", device.deviceName); + verifyDeviceReq.addProperty("totp", generateTotpCode(process.getProcess(), device)); + JsonObject verifyDeviceRes = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device/verify", + verifyDeviceReq, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + "totp"); + assertEquals(verifyDeviceRes.get("status").getAsString(), "OK"); + + // Start the actual tests for update device API: JsonObject body = new JsonObject(); // Missing userId/deviceName/skew/period { - Exception e = updateDeviceRequest(process, body); + Exception e = verifyTotpCodeRequest(process, body); checkFieldMissingErrorResponse(e, "userId"); body.addProperty("userId", ""); - e = updateDeviceRequest(process, body); + e = verifyTotpCodeRequest(process, body); checkFieldMissingErrorResponse(e, "totp"); - - body.addProperty("totp", ""); - e = updateDeviceRequest(process, body); - checkFieldMissingErrorResponse(e, "allowUnverifiedDevices"); } // Invalid userId/deviceName/skew/period { - body.addProperty("allowUnverifiedDevices", true); - Exception e = updateDeviceRequest(process, body); + body.addProperty("totp", ""); + Exception e = verifyTotpCodeRequest(process, body); checkResponseErrorContains(e, "userId cannot be empty"); // Note that this is not a field missing error body.addProperty("userId", device.userId); - e = updateDeviceRequest(process, body); + e = verifyTotpCodeRequest(process, body); checkResponseErrorContains(e, "totp must be 6 characters long"); // test totp of length 5: body.addProperty("totp", "12345"); - e = updateDeviceRequest(process, body); + e = verifyTotpCodeRequest(process, body); checkResponseErrorContains(e, "totp must be 6 characters long"); // test totp of length 8: body.addProperty("totp", "12345678"); - e = updateDeviceRequest(process, body); + e = verifyTotpCodeRequest(process, body); checkResponseErrorContains(e, "totp must be 6 characters long"); // but let's pass invalid code first @@ -178,10 +191,10 @@ public void testApi() throws Exception { assert res3.get("retryAfterMs") != null; // wait for cooldown to end (1s) - Thread.sleep(1000); + Thread.sleep(1300); // should pass now on valid code - String validTotp = TOTPRecipeTest.generateTotpCode(process.getProcess(), device); + String validTotp = generateTotpCode(process.getProcess(), device); body.addProperty("totp", validTotp); JsonObject res = HttpRequestForTesting.sendJsonPOSTRequest( process.getProcess(), @@ -210,7 +223,7 @@ public void testApi() throws Exception { assert res2.get("status").getAsString().equals("INVALID_TOTP_ERROR"); // Try with a new valid code during rate limiting: - body.addProperty("totp", TOTPRecipeTest.generateTotpCode(process.getProcess(), device)); + body.addProperty("totp", generateTotpCode(process.getProcess(), device)); res = HttpRequestForTesting.sendJsonPOSTRequest( process.getProcess(), "", @@ -235,7 +248,7 @@ public void testApi() throws Exception { null, Utils.getCdiVersionStringLatestForTests(), "totp"); - assert res5.get("status").getAsString().equals("TOTP_NOT_ENABLED_ERROR"); + assert res5.get("status").getAsString().equals("UNKNOWN_USER_ID_ERROR"); } process.kill(); diff --git a/src/test/java/io/supertokens/test/totp/api/VerifyTotpDeviceAPITest.java b/src/test/java/io/supertokens/test/totp/api/VerifyTotpDeviceAPITest.java index 6df604603..8a55255c9 100644 --- a/src/test/java/io/supertokens/test/totp/api/VerifyTotpDeviceAPITest.java +++ b/src/test/java/io/supertokens/test/totp/api/VerifyTotpDeviceAPITest.java @@ -84,7 +84,7 @@ public void testApi() throws Exception { return; } - FeatureFlagTestContent.getInstance(process.main).setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.TOTP }); + FeatureFlagTestContent.getInstance(process.main).setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.MFA }); // Setup user and devices: JsonObject createDeviceReq = new JsonObject(); @@ -106,7 +106,7 @@ public void testApi() throws Exception { assertEquals(createDeviceRes.get("status").getAsString(), "OK"); String secretKey = createDeviceRes.get("secret").getAsString(); - TOTPDevice device = new TOTPDevice("user-id", "deviceName", secretKey, 30, 0, false); + TOTPDevice device = new TOTPDevice("user-id", "deviceName", secretKey, 30, 0, false, System.currentTimeMillis()); // Start the actual tests for update device API: @@ -162,7 +162,10 @@ public void testApi() throws Exception { null, Utils.getCdiVersionStringLatestForTests(), "totp"); + assertEquals(3, res0.entrySet().size()); assert res0.get("status").getAsString().equals("INVALID_TOTP_ERROR"); + assertEquals(1, res0.get("currentNumberOfFailedAttempts").getAsInt()); + assertEquals(1, res0.get("maxNumberOfFailedAttempts").getAsInt()); // Check that rate limiting is triggered for the user: JsonObject res3 = HttpRequestForTesting.sendJsonPOSTRequest( @@ -238,7 +241,7 @@ public void testApi() throws Exception { null, Utils.getCdiVersionStringLatestForTests(), "totp"); - assert res5.get("status").getAsString().equals("TOTP_NOT_ENABLED_ERROR"); + assert res5.get("status").getAsString().equals("UNKNOWN_DEVICE_ERROR"); } process.kill(); diff --git a/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingStorageTest.java b/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingStorageTest.java index 26e5bf129..ca7e6ce1b 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingStorageTest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingStorageTest.java @@ -583,7 +583,8 @@ public void createUsersMapTheirIdsCheckRetrieveUseIdMappingsWithListOfUserIds() storage.createUserIdMapping(new AppIdentifier(null, null), superTokensUserId, externalUserId, null); } - HashMap response = storage.getUserIdMappingForSuperTokensIds(superTokensUserIdList); + HashMap response = storage.getUserIdMappingForSuperTokensIds( + new AppIdentifier(null, null), superTokensUserIdList); assertEquals(AuthRecipe.USER_PAGINATION_LIMIT, response.size()); for (int i = 0; i < response.size(); i++) { assertEquals(externalUserIdList.get(i), response.get(superTokensUserIdList.get(i))); @@ -606,7 +607,8 @@ public void testCallingGetUserIdMappingForSuperTokensIdsWithEmptyList() throws E UserIdMappingStorage storage = (UserIdMappingStorage) StorageLayer.getStorage(process.main); ArrayList emptyList = new ArrayList<>(); - HashMap response = storage.getUserIdMappingForSuperTokensIds(emptyList); + HashMap response = storage.getUserIdMappingForSuperTokensIds( + new AppIdentifier(null, null), emptyList); assertEquals(0, response.size()); process.kill(); @@ -631,7 +633,8 @@ public void testCallingGetUserIdMappingForSuperTokensIdsWhenNoMappingExists() th superTokensUserIdList.add(userInfo.getSupertokensUserId()); } - HashMap userIdMapping = storage.getUserIdMappingForSuperTokensIds(superTokensUserIdList); + HashMap userIdMapping = storage.getUserIdMappingForSuperTokensIds( + new AppIdentifier(null, null), superTokensUserIdList); assertEquals(0, userIdMapping.size()); process.kill(); @@ -668,7 +671,8 @@ public void create10UsersAndMap5UsersIds() throws Exception { } // retrieve UserIDMapping - HashMap response = storage.getUserIdMappingForSuperTokensIds(superTokensUserIdList); + HashMap response = storage.getUserIdMappingForSuperTokensIds( + new AppIdentifier(null, null), superTokensUserIdList); assertEquals(5, response.size()); // check that the last 5 users have their ids mapped diff --git a/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingTest.java b/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingTest.java index 9a6cfb33a..cfc9e6db2 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingTest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/UserIdMappingTest.java @@ -791,7 +791,7 @@ public void checkThatCreateUserIdMappingHasAllNonAuthRecipeChecks() throws Excep return; } - FeatureFlagTestContent.getInstance(process.main).setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.TOTP }); + FeatureFlagTestContent.getInstance(process.main).setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[] { EE_FEATURES.MFA }); // this list contains the package names for recipes which dont use UserIdMapping ArrayList nonAuthRecipesWhichDontNeedUserIdMapping = new ArrayList<>( diff --git a/src/test/java/io/supertokens/test/userIdMapping/api/MultitenantAPITest.java b/src/test/java/io/supertokens/test/userIdMapping/api/MultitenantAPITest.java index e3d65423d..118ecb737 100644 --- a/src/test/java/io/supertokens/test/userIdMapping/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/userIdMapping/api/MultitenantAPITest.java @@ -106,7 +106,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - config + null, null, config ) ); } @@ -126,7 +126,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - config + null, null, config ) ); } @@ -146,7 +146,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - config + null, null, config ) ); } @@ -166,7 +166,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - config + null, null, config ) ); } @@ -190,7 +190,8 @@ private JsonObject emailPasswordSignUp(TenantIdentifier tenantIdentifier, String return signUpResponse.getAsJsonObject("user"); } - private void successfulCreateUserIdMapping(TenantIdentifier tenantIdentifier, String supertokensUserId, String externalUserId) + private void successfulCreateUserIdMapping(TenantIdentifier tenantIdentifier, String supertokensUserId, + String externalUserId) throws HttpResponseException, IOException { JsonObject requestBody = new JsonObject(); requestBody.addProperty("superTokensUserId", supertokensUserId); @@ -203,7 +204,8 @@ private void successfulCreateUserIdMapping(TenantIdentifier tenantIdentifier, St assertEquals("OK", response.get("status").getAsString()); } - private void mappingAlreadyExistsWithCreateUserIdMapping(TenantIdentifier tenantIdentifier, String supertokensUserId, String externalUserId) + private void mappingAlreadyExistsWithCreateUserIdMapping(TenantIdentifier tenantIdentifier, + String supertokensUserId, String externalUserId) throws HttpResponseException, IOException { JsonObject requestBody = new JsonObject(); requestBody.addProperty("superTokensUserId", supertokensUserId); @@ -218,13 +220,18 @@ private void mappingAlreadyExistsWithCreateUserIdMapping(TenantIdentifier tenant private JsonObject getUserIdMapping(TenantIdentifier tenantIdentifier, String userId, String userIdType) throws HttpResponseException, IOException { + return getUserIdMapping(tenantIdentifier, userId, userIdType, SemVer.v3_0); + } + private JsonObject getUserIdMapping(TenantIdentifier tenantIdentifier, String userId, String userIdType, + SemVer version) + throws HttpResponseException, IOException { HashMap QUERY_PARAM = new HashMap<>(); QUERY_PARAM.put("userId", userId); QUERY_PARAM.put("userIdType", userIdType); JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", HttpRequestForTesting.getMultitenantUrl(tenantIdentifier, "/recipe/userid/map"), QUERY_PARAM, 1000, 1000, null, - SemVer.v3_0.get(), "useridmapping"); + version.get(), "useridmapping"); assertEquals("OK", response.get("status").getAsString()); return response; } @@ -417,6 +424,54 @@ public void testSameExternalIdAcrossUserPoolJustReturnsOneOfThem() throws Except } } + @Test + public void testSameExternalIdAcrossUserPoolJustReturnsOneOfThem_v5() throws Exception { + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + JsonObject user1 = emailPasswordSignUp(t1, "user@example.com", "password1"); + JsonObject user2 = emailPasswordSignUp(t2, "user@example.com", "password2"); + + ((UserIdMappingStorage)StorageLayer.getStorage(t1, process.getProcess())).createUserIdMapping( + t1.toAppIdentifier(), user1.get("id").getAsString(), "euserid", null); + + ((UserIdMappingStorage)StorageLayer.getStorage(t2, process.getProcess())).createUserIdMapping( + t2.toAppIdentifier(), user2.get("id").getAsString(), "euserid", null); + + { + JsonObject mapping = getUserIdMapping(t1, "euserid", "EXTERNAL"); + assert mapping.get("superTokensUserId").getAsString().equals(user1.get("id").getAsString()) + || mapping.get("superTokensUserId").getAsString().equals(user2.get("id").getAsString()); + } + { + JsonObject mapping = getUserIdMapping(t1, "euserid", "ANY"); + assert mapping.get("superTokensUserId").getAsString().equals(user1.get("id").getAsString()) + || mapping.get("superTokensUserId").getAsString().equals(user2.get("id").getAsString()); + } + + { + try { + JsonObject mapping = getUserIdMapping(t2, "euserid", "EXTERNAL", SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } + } + { + try { + JsonObject mapping = getUserIdMapping(t2, "euserid", "ANY", SemVer.v5_0); + fail(); + } catch (HttpResponseException e) { + assertEquals(403, e.statusCode); + } + } + } + @Test public void testUserIdFromDifferentAppIsAllowedForUserIdMapping() throws Exception { if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {