diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index f87766863..3daa3caa9 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -6,10 +6,8 @@ import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.RSAKeyProvider; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonPrimitive; +import com.google.gson.*; +import io.supertokens.ActiveUsers; import io.supertokens.Main; import io.supertokens.ProcessState; import io.supertokens.cronjobs.Cronjobs; @@ -21,6 +19,7 @@ import io.supertokens.httpRequest.HttpRequest; import io.supertokens.httpRequest.HttpResponseException; import io.supertokens.output.Logging; +import io.supertokens.pluginInterface.ActiveUsersStorage; import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -144,15 +143,50 @@ public Boolean getIsLicenseKeyPresent() { @Override public JsonObject getPaidFeatureStats() throws StorageQueryException { - JsonObject result = new JsonObject(); + JsonObject usageStats = new JsonObject(); EE_FEATURES[] features = getEnabledEEFeaturesFromDbOrCache(); - if (Arrays.stream(features).anyMatch(t -> t == EE_FEATURES.DASHBOARD_LOGIN)) { - JsonObject stats = new JsonObject(); - int userCount = StorageLayer.getDashboardStorage(main).getAllDashboardUsers().length; - stats.addProperty("user_count", userCount); - result.add(EE_FEATURES.DASHBOARD_LOGIN.toString(), stats); + ActiveUsersStorage activeUsersStorage = StorageLayer.getActiveUsersStorage(main); + + for (EE_FEATURES feature : features) { + if (feature == EE_FEATURES.DASHBOARD_LOGIN) { + JsonObject stats = new JsonObject(); + int userCount = StorageLayer.getDashboardStorage(main).getAllDashboardUsers().length; + stats.addProperty("user_count", userCount); + usageStats.add(EE_FEATURES.DASHBOARD_LOGIN.toString(), stats); + } + if (feature == EE_FEATURES.TOTP) { + JsonObject totpStats = new JsonObject(); + JsonArray totpMauArr = new JsonArray(); + + for (int i = 0; i < 30; i++) { + long now = System.currentTimeMillis(); + long today = now - (now % (24 * 60 * 60 * 1000L)); + long timestamp = today - (i * 24 * 60 * 60 * 1000L); + + int totpMau = activeUsersStorage.countUsersEnabledTotpAndActiveSince(timestamp); + totpMauArr.add(new JsonPrimitive(totpMau)); + } + + totpStats.add("maus", totpMauArr); + + int totpTotalUsers = activeUsersStorage.countUsersEnabledTotp(); + totpStats.addProperty("total_users", totpTotalUsers); + usageStats.add(EE_FEATURES.TOTP.toString(), totpStats); + } + } + + JsonArray mauArr = new JsonArray(); + for (int i = 0; i < 30; i++) { + long now = System.currentTimeMillis(); + long today = now - (now % (24 * 60 * 60 * 1000L)); + long timestamp = today - (i * 24 * 60 * 60 * 1000L); + + int mau = activeUsersStorage.countUsersActiveSince(timestamp); + mauArr.add(new JsonPrimitive(mau)); } - return result; + + usageStats.add("maus", mauArr); + return usageStats; } private EE_FEATURES[] verifyLicenseKey(String licenseKey) diff --git a/src/main/java/io/supertokens/featureflag/EE_FEATURES.java b/src/main/java/io/supertokens/featureflag/EE_FEATURES.java index 05e554dc6..820f2f73a 100644 --- a/src/main/java/io/supertokens/featureflag/EE_FEATURES.java +++ b/src/main/java/io/supertokens/featureflag/EE_FEATURES.java @@ -17,7 +17,8 @@ package io.supertokens.featureflag; public enum EE_FEATURES { - ACCOUNT_LINKING("account_linking"), MULTI_TENANCY("multi_tenancy"), TEST("test"), DASHBOARD_LOGIN("dashboard_login"); + ACCOUNT_LINKING("account_linking"), MULTI_TENANCY("multi_tenancy"), TEST("test"), DASHBOARD_LOGIN("dashboard_login"), + TOTP("totp"); 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 cbc47bc6d..2cf559eee 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -460,6 +460,24 @@ public int countUsersActiveSince(long time) throws StorageQueryException { } } + @Override + public int countUsersEnabledTotp() throws StorageQueryException { + try { + return ActiveUsersQueries.countUsersEnabledTotp(this); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + + @Override + public int countUsersEnabledTotpAndActiveSince(long time) throws StorageQueryException { + try { + return ActiveUsersQueries.countUsersEnabledTotpAndActiveSince(this, time); + } catch (SQLException e) { + throw new StorageQueryException(e); + } + } + @Override public SessionInfo getSessionInfo_Transaction(TransactionConnection con, String sessionHandle) throws StorageQueryException { diff --git a/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java index 43fc22f52..ec1672f27 100644 --- a/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java +++ b/src/main/java/io/supertokens/inmemorydb/queries/ActiveUsersQueries.java @@ -28,6 +28,31 @@ public static int countUsersActiveSince(Start start, long sinceTime) throws SQLE }); } + public static int countUsersEnabledTotp(Start start) throws SQLException, StorageQueryException { + String QUERY = "SELECT COUNT(*) as total FROM " + Config.getConfig(start).getTotpUsersTable(); + + return execute(start, QUERY, null, result -> { + if (result.next()) { + return result.getInt("total"); + } + return 0; + }); + } + + public static int countUsersEnabledTotpAndActiveSince(Start start, 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.last_active_time >= ?"; + + return execute(start, QUERY, pst -> pst.setLong(1, sinceTime), result -> { + if (result.next()) { + return result.getInt("total"); + } + return 0; + }); + } + public static int updateUserLastActive(Start start, String userId) throws SQLException, StorageQueryException { String QUERY = "INSERT INTO " + Config.getConfig(start).getUserLastActiveTable() + "(user_id, last_active_time) VALUES(?, ?) ON CONFLICT(user_id) DO UPDATE SET last_active_time = ?"; 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 76b3e09cf..bb04f7070 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/CreateOrUpdateTotpDeviceAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/CreateOrUpdateTotpDeviceAPI.java @@ -4,7 +4,6 @@ import java.security.NoSuchAlgorithmException; import com.google.gson.JsonObject; - import io.supertokens.Main; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -12,7 +11,9 @@ 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.useridmapping.UserIdMapping; import io.supertokens.totp.Totp; +import io.supertokens.useridmapping.UserIdType; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -59,6 +60,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject result = new JsonObject(); try { + // 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. + UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(super.main, userId, UserIdType.ANY); + if (userIdMapping != null) { + userId = userIdMapping.superTokensUserId; + } + TOTPDevice device = Totp.registerDevice(main, userId, deviceName, skew, period); result.addProperty("status", "OK"); @@ -93,6 +101,13 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject result = new JsonObject(); try { + // 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. + UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(super.main, userId, UserIdType.ANY); + if (userIdMapping != null) { + userId = userIdMapping.superTokensUserId; + } + Totp.updateDeviceName(main, userId, existingDeviceName, newDeviceName); result.addProperty("status", "OK"); 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 e8a9d156d..9bda4ed27 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/GetTotpDevicesAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/GetTotpDevicesAPI.java @@ -10,7 +10,9 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.totp.TOTPDevice; import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; +import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import io.supertokens.totp.Totp; +import io.supertokens.useridmapping.UserIdType; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -40,6 +42,13 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO JsonObject result = new JsonObject(); try { + // 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. + UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(super.main, userId, UserIdType.ANY); + if (userIdMapping != null) { + userId = userIdMapping.superTokensUserId; + } + TOTPDevice[] devices = Totp.getDevices(main, userId); JsonArray devicesArray = new JsonArray(); 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 b2a079694..634e6fe20 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/RemoveTotpDeviceAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/RemoveTotpDeviceAPI.java @@ -10,7 +10,9 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; +import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import io.supertokens.totp.Totp; +import io.supertokens.useridmapping.UserIdType; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -46,6 +48,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject result = new JsonObject(); try { + // 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. + UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(super.main, userId, UserIdType.ANY); + if (userIdMapping != null) { + userId = userIdMapping.superTokensUserId; + } + Totp.removeDevice(main, userId, deviceName); result.addProperty("status", "OK"); 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 027eaca5d..af9400863 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpAPI.java @@ -9,9 +9,11 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; +import io.supertokens.pluginInterface.useridmapping.UserIdMapping; 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.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -49,6 +51,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject result = new JsonObject(); try { + // 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. + UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(super.main, userId, UserIdType.ANY); + if (userIdMapping != null) { + userId = userIdMapping.superTokensUserId; + } + Totp.verifyCode(main, userId, totp, allowUnverifiedDevices); result.addProperty("status", "OK"); 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 00f38d40f..f1dd1ba0a 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpDeviceAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpDeviceAPI.java @@ -10,9 +10,11 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; import io.supertokens.pluginInterface.totp.exception.UnknownDeviceException; +import io.supertokens.pluginInterface.useridmapping.UserIdMapping; 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.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -52,6 +54,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject result = new JsonObject(); try { + // 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. + UserIdMapping userIdMapping = io.supertokens.useridmapping.UserIdMapping.getUserIdMapping(super.main, userId, UserIdType.ANY); + if (userIdMapping != null) { + userId = userIdMapping.superTokensUserId; + } + boolean isNewlyVerified = Totp.verifyDevice(main, userId, deviceName, totp); result.addProperty("status", "OK"); diff --git a/src/test/java/io/supertokens/test/FeatureFlagTest.java b/src/test/java/io/supertokens/test/FeatureFlagTest.java index bce7b57b6..926595b44 100644 --- a/src/test/java/io/supertokens/test/FeatureFlagTest.java +++ b/src/test/java/io/supertokens/test/FeatureFlagTest.java @@ -16,6 +16,7 @@ package io.supertokens.test; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.supertokens.ProcessState; import io.supertokens.featureflag.FeatureFlag; @@ -111,4 +112,101 @@ public void testThatCallingGetFeatureFlagAPIReturnsEmptyArray() throws Exception process.kill(); Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + private final String OPAQUE_KEY_WITH_TOTP_FEATURE = "pXhNK=nYiEsb6gJEOYP2kIR6M0kn4XLvNqcwT1XbX8xHtm44K-lQfGCbaeN0Ieeza39fxkXr=tiiUU=DXxDH40Y=4FLT4CE-rG1ETjkXxO4yucLpJvw3uSegPayoISGL"; + + @Test + public void testThatCallingGetFeatureFlagAPIReturnsTotpStats() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + FeatureFlag.getInstance(process.main).setLicenseKeyAndSyncFeatures(OPAQUE_KEY_WITH_TOTP_FEATURE); + + // Get the stats without any users/activity + { + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/ee/featureflag", + null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion(), ""); + 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(); + + assert features.size() == 1; + assert features.get(0).getAsString().equals("totp"); + assert maus.size() == 30; + 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(); + + assert totpMaus.size() == 30; + assert totpMaus.get(0).getAsInt() == 0; + assert totpMaus.get(29).getAsInt() == 0; + + assert totalTotpUsers == 0; + } + + // First register 2 users for emailpassword recipe. + // This also marks them as active. + JsonObject signUpResponse = Utils.signUpRequest_2_5(process, "random@gmail.com", "validPass123"); + assert signUpResponse.get("status").getAsString().equals("OK"); + + 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.getCdiVersionLatestForTests(), + "totp"); + assert res.get("status").getAsString().equals("OK"); + + // Now check the stats again: + { + JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "", + "http://localhost:3567/ee/featureflag", + null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion(), ""); + 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(); + + assert features.size() == 1; + assert features.get(0).getAsString().equals("totp"); + assert maus.size() == 30; + 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(); + + assert totpMaus.size() == 30; + assert totpMaus.get(0).getAsInt() == 1; // only 1 user has TOTP enabled + assert totpMaus.get(29).getAsInt() == 1; + + assert totalTotpUsers == 1; + } + + process.kill(); + Assert.assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } diff --git a/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest2_7.java b/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest2_7.java index d47ae0a79..bd4b97285 100644 --- a/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest2_7.java +++ b/src/test/java/io/supertokens/test/emailpassword/api/SignUpAPITest2_7.java @@ -141,7 +141,7 @@ public void testGoodInput() throws Exception { assertNotNull(signUpUser.get("id")); int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), beforeSignUpTs); - assert (activeUsers == 0); + assert (activeUsers == 1); UserInfo user = StorageLayer.getEmailPasswordStorage(process.getProcess()) .getUserInfoUsingEmail("random@gmail.com"); 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 fd15bd114..154d96bf7 100644 --- a/src/test/java/io/supertokens/test/totp/api/CreateTotpDeviceAPITest.java +++ b/src/test/java/io/supertokens/test/totp/api/CreateTotpDeviceAPITest.java @@ -142,5 +142,4 @@ public void testApi() throws Exception { process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } - } diff --git a/src/test/java/io/supertokens/test/totp/api/TotpUserIdMappingTest.java b/src/test/java/io/supertokens/test/totp/api/TotpUserIdMappingTest.java new file mode 100644 index 000000000..3f9970dbb --- /dev/null +++ b/src/test/java/io/supertokens/test/totp/api/TotpUserIdMappingTest.java @@ -0,0 +1,176 @@ +package io.supertokens.test.totp.api; + +import com.google.gson.JsonObject; +import io.supertokens.ProcessState; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.pluginInterface.emailpassword.UserInfo; +import io.supertokens.pluginInterface.totp.TOTPDevice; +import io.supertokens.test.httpRequest.HttpResponseException; +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.totp.TOTPRecipeTest; +import io.supertokens.useridmapping.UserIdMapping; + +import static org.junit.Assert.assertNotNull; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +public class TotpUserIdMappingTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void testExternalUserIdTranslation() 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 body = new JsonObject(); + + UserInfo user = EmailPassword.signUp(process.main, "test@example.com", "testPass123"); + String superTokensUserId = user.id; + String externalUserId = "external-user-id"; + + // Create user id mapping first: + UserIdMapping.createUserIdMapping(process.main, superTokensUserId, externalUserId, null, false); + + body.addProperty("userId", externalUserId); + body.addProperty("deviceName", "d1"); + body.addProperty("skew", 0); + body.addProperty("period", 30); + + // Register 1st device + JsonObject res1 = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device", + body, + 1000, + 1000, + null, + Utils.getCdiVersionLatestForTests(), + "totp"); + assert res1.get("status").getAsString().equals("OK"); + String d1Secret = res1.get("secret").getAsString(); + TOTPDevice device1 = new TOTPDevice(externalUserId, "deviceName", d1Secret, 30, 0, false); + + body.addProperty("deviceName", "d2"); + + JsonObject res2 = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device", + body, + 1000, + 1000, + null, + Utils.getCdiVersionLatestForTests(), + "totp"); + assert res2.get("status").getAsString().equals("OK"); + String d2Secret = res2.get("secret").getAsString(); + TOTPDevice device2 = new TOTPDevice(externalUserId, "deviceName", d2Secret, 30, 0, false); + + // Verify d1 but not d2: + JsonObject verifyD1Input = new JsonObject(); + verifyD1Input.addProperty("userId", externalUserId); + String d1Totp = TOTPRecipeTest.generateTotpCode(process.getProcess(), device1); + verifyD1Input.addProperty("deviceName", "d1"); + verifyD1Input.addProperty("totp", d1Totp ); + + JsonObject verifyD1Res = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device/verify", + verifyD1Input, + 1000, + 1000, + null, + Utils.getCdiVersionLatestForTests(), + "totp"); + + 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); + + JsonObject loginRes = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/verify", + loginInput, + 1000, + 1000, + null, + Utils.getCdiVersionLatestForTests(), + "totp"); + + assert loginRes.get("status").getAsString().equals("OK"); + + // Change the name of d1 to d3: + JsonObject updateDeviceNameInput = new JsonObject(); + updateDeviceNameInput.addProperty("userId", externalUserId); + updateDeviceNameInput.addProperty("existingDeviceName", "d1"); + updateDeviceNameInput.addProperty("newDeviceName", "d3"); + + JsonObject updateDeviceNameRes = HttpRequestForTesting.sendJsonPUTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device", + updateDeviceNameInput, + 1000, + 1000, + null, + Utils.getCdiVersionLatestForTests(), + "totp"); + + assert updateDeviceNameRes.get("status").getAsString().equals("OK"); + + // Delete d3: + JsonObject deleteDeviceInput = new JsonObject(); + deleteDeviceInput.addProperty("userId", externalUserId); + deleteDeviceInput.addProperty("deviceName", "d3"); + + JsonObject deleteDeviceRes = HttpRequestForTesting.sendJsonPOSTRequest( + process.getProcess(), + "", + "http://localhost:3567/recipe/totp/device/remove", + deleteDeviceInput, + 1000, + 1000, + null, + Utils.getCdiVersionLatestForTests(), + "totp"); + + + assert deleteDeviceRes.get("status").getAsString().equals("OK"); + assert deleteDeviceRes.get("didDeviceExist").getAsBoolean() == true; + + } +}