diff --git a/CHANGELOG.md b/CHANGELOG.md index 1701e98ec..71b6d5c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,42 @@ 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). +## [8.0.0] - 2023-11-29 + +### 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 + +## [7.0.18] - 2024-02-19 + +- Fixes vulnerabilities in dependencies +- Updates telemetry payload +- Fixes Active User tracking to use the right storage + ## [7.0.17] - 2024-02-06 - Fixes issue where error logs were printed to StdOut instead of StdErr. diff --git a/build.gradle b/build.gradle index e43503f26..806add897 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" } // } //} -version = "7.0.17" +version = "8.0.0" repositories { @@ -33,22 +33,22 @@ dependencies { implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml - implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.14.0' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.14.0' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1' // https://mvnrepository.com/artifact/ch.qos.logback/logback-classic - implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3' + implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14' // https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core - implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.1' + implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18' // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' // https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc - implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.30.1' + implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.45.1.0' // https://mvnrepository.com/artifact/org.mindrot/jbcrypt implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4' diff --git a/cli/build.gradle b/cli/build.gradle index 904dc0065..52e2ab2d5 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -19,10 +19,10 @@ dependencies { implementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml - implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.10.0' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.0' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1' // https://mvnrepository.com/artifact/de.mkammerer/argon2-jvm implementation group: 'de.mkammerer', name: 'argon2-jvm', version: '2.11' diff --git a/cli/implementationDependencies.json b/cli/implementationDependencies.json index 645cacaf4..665c92fff 100644 --- a/cli/implementationDependencies.json +++ b/cli/implementationDependencies.json @@ -7,29 +7,29 @@ "src": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.10.0/jackson-dataformat-yaml-2.10.0.jar", - "name": "Jackson Dataformat 2.10.0", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.10.0/jackson-dataformat-yaml-2.10.0-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1.jar", + "name": "Jackson Dataformat 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.24/snakeyaml-1.24.jar", - "name": "SnakeYAML 1.24", - "src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.24/snakeyaml-1.24-sources.jar" + "jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar", + "name": "SnakeYAML 2.2", + "src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.10.0/jackson-core-2.10.0.jar", - "name": "Jackson core 2.10.0", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.10.0/jackson-core-2.10.0-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1.jar", + "name": "Jackson core 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.10.0/jackson-databind-2.10.0.jar", - "name": "Jackson databind 2.10.0", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.10.0/jackson-databind-2.10.0-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1.jar", + "name": "Jackson databind 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.10.0/jackson-annotations-2.10.0.jar", - "name": "Jackson annotation 2.10.0", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.10.0/jackson-annotations-2.10.0-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1.jar", + "name": "Jackson annotation 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1-sources.jar" }, { "jar": "https://repo1.maven.org/maven2/de/mkammerer/argon2-jvm/2.11/argon2-jvm-2.11.jar", diff --git a/cli/jar/cli.jar b/cli/jar/cli.jar index 2c1d99d03..679236a42 100644 Binary files a/cli/jar/cli.jar and b/cli/jar/cli.jar differ 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/downloader/jar/downloader.jar b/downloader/jar/downloader.jar index 2b9e3569f..4c8bac4ca 100644 Binary files a/downloader/jar/downloader.jar and b/downloader/jar/downloader.jar differ diff --git a/ee/build.gradle b/ee/build.gradle index 94aeed97c..9e91d7a57 100644 --- a/ee/build.gradle +++ b/ee/build.gradle @@ -35,10 +35,10 @@ dependencies { testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.1.0' // https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core - testImplementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.1' + testImplementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18' // https://mvnrepository.com/artifact/ch.qos.logback/logback-classic - testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3' + testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14' // https://mvnrepository.com/artifact/com.google.code.gson/gson testImplementation group: 'com.google.code.gson', name: 'gson', version: '2.3.1' @@ -46,10 +46,10 @@ dependencies { testImplementation 'com.tngtech.archunit:archunit-junit4:0.22.0' // https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml - testImplementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.14.0' + testImplementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core - testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.14.0' + testImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1' testImplementation group: 'org.jetbrains', name: 'annotations', version: '13.0' } diff --git a/ee/jar/ee.jar b/ee/jar/ee.jar index 842a252ec..1795fb9d7 100644 Binary files a/ee/jar/ee.jar and b/ee/jar/ee.jar differ diff --git a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java index 141009818..cac2c5cb9 100644 --- a/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java +++ b/ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java @@ -185,44 +185,39 @@ 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 = 0; i < 30; i++) { - long today = now - (now % (24 * 60 * 60 * 1000L)); - long timestamp = today - (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() @@ -273,6 +268,7 @@ private JsonObject getMultiTenancyStats() } private JsonObject getAccountLinkingStats() throws StorageQueryException { + // 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); boolean usesAccountLinking = false; @@ -288,7 +284,7 @@ private JsonObject getAccountLinkingStats() throws StorageQueryException { if (!usesAccountLinking) { result.addProperty("totalUserCountWithMoreThanOneLoginMethod", 0); JsonArray mauArray = new JsonArray(); - for (int i = 0; i < 30; i++) { + for (int i = 0; i < 31; i++) { mauArray.add(new JsonPrimitive(0)); } result.add("mauWithMoreThanOneLoginMethod", mauArray); @@ -296,17 +292,18 @@ private JsonObject getAccountLinkingStats() throws StorageQueryException { } int totalUserCountWithMoreThanOneLoginMethod = 0; - int[] maus = new int[30]; + int[] maus = new int[31]; long now = System.currentTimeMillis(); - long today = now - (now % (24 * 60 * 60 * 1000L)); for (Storage storage : storages) { totalUserCountWithMoreThanOneLoginMethod += ((AuthRecipeStorage)storage).getUsersCountWithMoreThanOneLoginMethod(this.appIdentifier); - for (int i = 0; i < 30; i++) { - long timestamp = today - (i * 24 * 60 * 60 * 1000L); - maus[i] += ((ActiveUsersStorage)storage).countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(appIdentifier, timestamp); + for (int i = 1; i <= 31; i++) { + long timestamp = now - (i * 24 * 60 * 60 * 1000L); + + // `maus[i-1]` because i starts from 1 + maus[i-1] += ((ActiveUsersStorage)storage).countUsersThatHaveMoreThanOneLoginMethodAndActiveSince(appIdentifier, timestamp); } } @@ -317,10 +314,10 @@ private JsonObject getAccountLinkingStats() throws StorageQueryException { private JsonArray getMAUs() throws StorageQueryException, TenantOrAppNotFoundException { 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); + long now = System.currentTimeMillis(); + + for (int i = 1; i <= 31; i++) { + long timestamp = now - (i * 24 * 60 * 60 * 1000L); ActiveUsersStorage activeUsersStorage = (ActiveUsersStorage) StorageLayer.getStorage( this.appIdentifier.getAsPublicTenantIdentifier(), main); int mau = activeUsersStorage.countUsersActiveSince(this.appIdentifier, timestamp); @@ -355,8 +352,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) { @@ -570,4 +567,4 @@ public static void resetLisenseCheckRequests() { } -} \ No newline at end of file +} diff --git a/ee/src/test/java/io/supertokens/ee/test/EETest.java b/ee/src/test/java/io/supertokens/ee/test/EETest.java index 3a896b253..7418b24ec 100644 --- a/ee/src/test/java/io/supertokens/ee/test/EETest.java +++ b/ee/src/test/java/io/supertokens/ee/test/EETest.java @@ -1326,7 +1326,7 @@ protected URLConnection openConnection(URL u) { JsonObject paidFeatureUsageStats = j.getAsJsonObject("paidFeatureUsageStats"); JsonArray mauArr = paidFeatureUsageStats.get("maus").getAsJsonArray(); assertEquals(paidFeatureUsageStats.entrySet().size(), 1); - assertEquals(mauArr.size(), 30); + assertEquals(mauArr.size(), 31); assertEquals(mauArr.get(0).getAsInt(), 0); assertEquals(mauArr.get(29).getAsInt(), 0); } 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/ee/src/test/java/io/supertokens/ee/test/api/GetFeatureFlagAPITest.java b/ee/src/test/java/io/supertokens/ee/test/api/GetFeatureFlagAPITest.java index d2932f619..efd133a86 100644 --- a/ee/src/test/java/io/supertokens/ee/test/api/GetFeatureFlagAPITest.java +++ b/ee/src/test/java/io/supertokens/ee/test/api/GetFeatureFlagAPITest.java @@ -54,7 +54,7 @@ public void testRetrievingFeatureFlagInfoWhenNoLicenseKeyIsSet() throws Exceptio if (StorageLayer.getStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) { JsonArray mauArr = usageStats.get("maus").getAsJsonArray(); assertEquals(1, usageStats.entrySet().size()); - assertEquals(30, mauArr.size()); + assertEquals(31, mauArr.size()); assertEquals(0, mauArr.get(0).getAsInt()); assertEquals(0, mauArr.get(29).getAsInt()); } else { @@ -92,7 +92,7 @@ public void testRetrievingFeatureFlagInfoWhenLicenseKeyIsSet() throws Exception if (StorageLayer.getStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) { JsonArray mauArr = usageStats.get("maus").getAsJsonArray(); assertEquals(1, usageStats.entrySet().size()); - assertEquals(30, mauArr.size()); + assertEquals(31, mauArr.size()); assertEquals(0, mauArr.get(0).getAsInt()); assertEquals(0, mauArr.get(29).getAsInt()); } else { diff --git a/implementationDependencies.json b/implementationDependencies.json index e0ab94e68..ec4da266a 100644 --- a/implementationDependencies.json +++ b/implementationDependencies.json @@ -7,54 +7,54 @@ "src": "https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.14.2/jackson-dataformat-yaml-2.14.2.jar", - "name": "Jackson Dataformat 2.14.2", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.14.2/jackson-dataformat-yaml-2.14.2-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1.jar", + "name": "Jackson Dataformat 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.16.1/jackson-dataformat-yaml-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.33/snakeyaml-1.33.jar", - "name": "SnakeYAML 1.33", - "src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.33/snakeyaml-1.33-sources.jar" + "jar": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar", + "name": "SnakeYAML 2.2", + "src": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.14.2/jackson-core-2.14.2.jar", - "name": "Jackson core 2.14.2", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.14.2/jackson-core-2.14.2-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1.jar", + "name": "Jackson core 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.16.1/jackson-core-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.14.2/jackson-databind-2.14.2.jar", - "name": "Jackson databind 2.14.2", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.14.2/jackson-databind-2.14.2-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1.jar", + "name": "Jackson databind 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.16.1/jackson-databind-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.14.2/jackson-annotations-2.14.2.jar", - "name": "Jackson annotation 2.14.2", - "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.14.2/jackson-annotations-2.14.2-sources.jar" + "jar": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1.jar", + "name": "Jackson annotation 2.16.1", + "src": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.16.1/jackson-annotations-2.16.1-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar", - "name": "Logback classic 1.2.3", - "src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3-sources.jar" + "jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.4.14/logback-classic-1.4.14.jar", + "name": "Logback classic 1.4.14", + "src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.4.14/logback-classic-1.4.14-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar", - "name": "Logback core 1.2.3", - "src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3-sources.jar" + "jar": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.4.14/logback-core-1.4.14.jar", + "name": "Logback core 1.4.14", + "src": "https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.4.14/logback-core-1.4.14-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar", - "name": "SLF4j API 1.7.25", - "src": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25-sources.jar" + "jar": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7.jar", + "name": "SLF4j API 2.0.7", + "src": "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.1/tomcat-annotations-api-10.1.1.jar", - "name": "Tomcat annotations API 10.1.1", - "src": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.1/tomcat-annotations-api-10.1.1-sources.jar" + "jar": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.18/tomcat-annotations-api-10.1.18.jar", + "name": "Tomcat annotations API 10.1.18", + "src": "https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-annotations-api/10.1.18/tomcat-annotations-api-10.1.18-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.1/tomcat-embed-core-10.1.1.jar", + "jar": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18.jar", "name": "Tomcat embed core API 10.1.1", - "src": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.1/tomcat-embed-core-10.1.1-sources.jar" + "src": "https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-core/10.1.18/tomcat-embed-core-10.1.18-sources.jar" }, { "jar": "https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar", @@ -67,13 +67,13 @@ "src": "https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar" }, { - "jar": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.30.1/sqlite-jdbc-3.30.1.jar", - "name": "SQLite JDBC Driver 3.30.1", - "src": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.30.1/sqlite-jdbc-3.30.1-sources.jar" + "jar": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar", + "name": "SQLite JDBC Driver 3.45.1.0", + "src": "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0-sources.jar" }, { "jar": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4.jar", - "name": "SQLite JDBC Driver 3.30.1", + "name": "JBCrypt 0.4", "src": "https://repo1.maven.org/maven2/org/mindrot/jbcrypt/0.4/jbcrypt-0.4-sources.jar" }, { 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-7.0.17.jar b/jar/core-7.0.18.jar similarity index 89% rename from jar/core-7.0.17.jar rename to jar/core-7.0.18.jar index 0d3f4c19a..4d0695612 100644 Binary files a/jar/core-7.0.17.jar and b/jar/core-7.0.18.jar differ diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index a5fdc62cd..e9d4c148c 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": [ - "4.0" + "5.0" ] } \ No newline at end of file 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 ade17dabb..1c3a2621a 100644 --- a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java +++ b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java @@ -338,7 +338,7 @@ public static LinkAccountsResult linkAccounts(Main main, AppIdentifierWithStorag UnknownUserIdException, TenantOrAppNotFoundException, FeatureNotEnabledException { if (Arrays.stream(FeatureFlag.getInstance(main, appIdentifierWithStorage).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."); } @@ -544,7 +544,7 @@ public static CreatePrimaryUserResult createPrimaryUser(Main main, FeatureNotEnabledException { if (Arrays.stream(FeatureFlag.getInstance(main, appIdentifierWithStorage).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."); } @@ -934,7 +934,6 @@ private static void deleteNonAuthRecipeUser(TransactionConnection con, AppIdenti .deleteAllRolesForUser_Transaction(con, appIdentifierWithStorage, userId); appIdentifierWithStorage.getActiveUsersStorage() .deleteUserActive_Transaction(con, appIdentifierWithStorage, userId); - appIdentifierWithStorage.getTOTPStorage().removeUser_Transaction(con, appIdentifierWithStorage, userId); } private static void deleteAuthRecipeUser(TransactionConnection con, @@ -975,6 +974,8 @@ public static boolean deleteNonAuthRecipeUser(TenantIdentifierWithStorage .removeUser(tenantIdentifierWithStorage, userId); finalDidExist = finalDidExist || didExist; + finalDidExist = finalDidExist || didExist; + return finalDidExist; } diff --git a/src/main/java/io/supertokens/cronjobs/telemetry/Telemetry.java b/src/main/java/io/supertokens/cronjobs/telemetry/Telemetry.java index 727d2f5ec..215024858 100644 --- a/src/main/java/io/supertokens/cronjobs/telemetry/Telemetry.java +++ b/src/main/java/io/supertokens/cronjobs/telemetry/Telemetry.java @@ -16,20 +16,26 @@ package io.supertokens.cronjobs.telemetry; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import io.supertokens.Main; import io.supertokens.ProcessState; +import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.config.Config; import io.supertokens.cronjobs.CronTask; import io.supertokens.cronjobs.CronTaskTest; +import io.supertokens.dashboard.Dashboard; import io.supertokens.httpRequest.HttpRequest; import io.supertokens.httpRequest.HttpRequestMocking; import io.supertokens.pluginInterface.ActiveUsersStorage; import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.dashboard.DashboardUser; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; @@ -90,14 +96,55 @@ protected void doTaskPerApp(AppIdentifier app) throws Exception { json.addProperty("telemetryId", telemetryId.value); json.addProperty("superTokensVersion", coreVersion); + json.addProperty("appId", app.getAppId()); + json.addProperty("connectionUriDomain", app.getConnectionUriDomain()); + if (StorageLayer.getBaseStorage(main).getType() == STORAGE_TYPE.SQL) { - ActiveUsersStorage activeUsersStorage = (ActiveUsersStorage) StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main); - json.addProperty("mau", activeUsersStorage.countUsersActiveSince(app, System.currentTimeMillis() - 30 * 24 * 3600 * 1000L)); + { // Users count across all tenants + Storage[] storages = StorageLayer.getStoragesForApp(main, app); + AppIdentifierWithStorage appIdentifierWithAllTenantStorages = new AppIdentifierWithStorage( + app.getConnectionUriDomain(), app.getAppId(), + StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main), storages + ); + + json.addProperty("usersCount", + AuthRecipe.getUsersCountAcrossAllTenants(appIdentifierWithAllTenantStorages, null)); + } + + { // Dashboard user emails + // Dashboard APIs are app specific and are always stored on the public tenant + DashboardUser[] dashboardUsers = Dashboard.getAllDashboardUsers( + app.withStorage(StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main)), main); + JsonArray dashboardUserEmails = new JsonArray(); + for (DashboardUser user : dashboardUsers) { + dashboardUserEmails.add(new JsonPrimitive(user.email)); + } + + json.add("dashboardUserEmails", dashboardUserEmails); + } + + { // MAUs + // Active users are always tracked on the public tenant, so we use the public tenant's storage + ActiveUsersStorage activeUsersStorage = (ActiveUsersStorage) StorageLayer.getStorage( + app.getAsPublicTenantIdentifier(), main); + + JsonArray mauArr = new JsonArray(); + + long now = System.currentTimeMillis(); + + for (int i = 1; i <= 31; i++) { + long timestamp = now - (i * 24 * 60 * 60 * 1000L); + int mau = activeUsersStorage.countUsersActiveSince(app, timestamp); + mauArr.add(new JsonPrimitive(mau)); + } + + json.add("maus", mauArr); + } } else { - json.addProperty("mau", -1); + json.addProperty("usersCount", -1); + json.add("dashboardUserEmails", new JsonArray()); + json.add("maus", new JsonArray()); } - json.addProperty("appId", app.getAppId()); - json.addProperty("connectionUriDomain", app.getConnectionUriDomain()); String url = "https://api.supertokens.io/0/st/telemetry"; @@ -105,7 +152,7 @@ protected void doTaskPerApp(AppIdentifier app) throws Exception { // wants // to use this) if (!Main.isTesting || HttpRequestMocking.getInstance(main).getMockURL(REQUEST_ID, url) != null) { - HttpRequest.sendJsonPOSTRequest(main, REQUEST_ID, url, json, 10000, 10000, 4); + HttpRequest.sendJsonPOSTRequest(main, REQUEST_ID, url, json, 10000, 10000, 5); ProcessState.getInstance(main).addState(ProcessState.PROCESS_STATE.SENT_TELEMETRY, null); } } diff --git a/src/main/java/io/supertokens/emailpassword/EmailPassword.java b/src/main/java/io/supertokens/emailpassword/EmailPassword.java index 0c7fb08a0..97772a433 100644 --- a/src/main/java/io/supertokens/emailpassword/EmailPassword.java +++ b/src/main/java/io/supertokens/emailpassword/EmailPassword.java @@ -110,14 +110,39 @@ public static AuthRecipeUserInfo signUp(TenantIdentifierWithStorage tenantIdenti .createHashWithSalt(tenantIdentifierWithStorage.toAppIdentifier(), password); while (true) { - String userId = Utils.getUUID(); long timeJoined = System.currentTimeMillis(); try { - return tenantIdentifierWithStorage.getEmailPasswordStorage() + AuthRecipeUserInfo newUser = tenantIdentifierWithStorage.getEmailPasswordStorage() .signUp(tenantIdentifierWithStorage, userId, email, hashedPassword, timeJoined); + if (Utils.isFakeEmail(email)) { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + try { + + tenantIdentifierWithStorage.getEmailVerificationStorage() + .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + newUser.getSupertokensUserId(), email, true); + tenantIdentifierWithStorage.getEmailVerificationStorage() + .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 2cb372551..c237811c5 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -26,8 +26,6 @@ import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; -import io.supertokens.pluginInterface.bulkimport.BulkImportStorage; -import io.supertokens.pluginInterface.bulkimport.BulkImportUser; import io.supertokens.pluginInterface.dashboard.DashboardSearchTags; import io.supertokens.pluginInterface.dashboard.DashboardSessionInfo; import io.supertokens.pluginInterface.dashboard.DashboardUser; @@ -71,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; @@ -104,7 +102,7 @@ public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage, UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, TOTPSQLStorage, ActiveUsersStorage, - DashboardSQLStorage, AuthRecipeSQLStorage, BulkImportStorage { + ActiveUsersSQLStorage, DashboardSQLStorage, AuthRecipeSQLStorage { private static final Object appenderLock = new Object(); private static final String APP_ID_KEY_NAME = "app_id"; @@ -522,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); } @@ -690,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); @@ -1217,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 { @@ -2588,26 +2568,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); } } @@ -2704,7 +2714,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 { @@ -2719,7 +2729,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(), @@ -2880,7 +2890,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,9 +2965,18 @@ public UserIdMapping[] getUserIdMapping_Transaction(TransactionConnection con, A } @Override - public void addBulkImportUsers(AppIdentifier appIdentifier, ArrayList users) throws StorageQueryException { + 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 { - BulkImportQueries.insertBulkImportUsers(this, users); + 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 9ce5cf561..e27c3ea16 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..732655c76 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() @@ -142,4 +109,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/BulkImportQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/BulkImportQueries.java deleted file mode 100644 index aeb6fe9e6..000000000 --- a/src/main/java/io/supertokens/inmemorydb/queries/BulkImportQueries.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2024, 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; - -import io.supertokens.inmemorydb.config.Config; -import io.supertokens.pluginInterface.bulkimport.BulkImportUser; -import io.supertokens.pluginInterface.exceptions.StorageQueryException; - -import static io.supertokens.inmemorydb.PreparedStatementValueSetter.NO_OP_SETTER; -import static io.supertokens.inmemorydb.QueryExecutorTemplate.update; - -import java.sql.SQLException; -import java.util.ArrayList; - -import io.supertokens.inmemorydb.Start; - -public class BulkImportQueries { - static String getQueryToCreateBulkImportUsersTable(Start start) { - return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getBulkImportUsersTable() + " (" - + "id CHAR(36) PRIMARY KEY," - + "raw_data TEXT NOT NULL," - + "status VARCHAR(128) NOT NULL DEFAULT 'NEW'," - + "error_msg TEXT," - + "created_at TIMESTAMP DEFAULT (strftime('%s', 'now'))," - + "updated_at TIMESTAMP DEFAULT (strftime('%s', 'now'))" - + " );"; - } - - public static String getQueryToCreateStatusUpdatedAtIndex(Start start) { - return "CREATE INDEX IF NOT EXISTS bulk_import_users_status_updated_at_index ON " - + Config.getConfig(start).getBulkImportUsersTable() + " (status, updated_at)"; - } - - public static void insertBulkImportUsers(Start start, ArrayList users) - throws SQLException, StorageQueryException { - StringBuilder queryBuilder = new StringBuilder( - "INSERT INTO " + Config.getConfig(start).getBulkImportUsersTable() + " (id, raw_data) VALUES "); - for (BulkImportUser user : users) { - queryBuilder.append("('") - .append(user.id) - .append("', '") - .append(user.toString()) - .append("')"); - - if (user != users.get(users.size() - 1)) { - queryBuilder.append(","); - } - } - queryBuilder.append(";"); - update(start, queryBuilder.toString(), NO_OP_SETTER); - } -} - diff --git a/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java b/src/main/java/io/supertokens/inmemorydb/queries/GeneralQueries.java index 4ac2f32a9..0f519dabf 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), @@ -406,13 +416,6 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc // index: update(start, TOTPQueries.getQueryToCreateUsedCodesExpiryTimeIndex(start), NO_OP_SETTER); } - - if (!doesTableExists(start, Config.getConfig(start).getBulkImportUsersTable())) { - getInstance(main).addState(CREATING_NEW_TABLE, null); - update(start, BulkImportQueries.getQueryToCreateBulkImportUsersTable(start), NO_OP_SETTER); - // index: - update(start, BulkImportQueries.getQueryToCreateStatusUpdatedAtIndex(start), NO_OP_SETTER); - } } @@ -1517,6 +1520,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/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 e97d3edf1..2cb068855 100644 --- a/src/main/java/io/supertokens/multitenancy/Multitenancy.java +++ b/src/main/java/io/supertokens/multitenancy/Multitenancy.java @@ -147,6 +147,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 f4003e302..6131006f0 100644 --- a/src/main/java/io/supertokens/passwordless/Passwordless.java +++ b/src/main/java/io/supertokens/passwordless/Passwordless.java @@ -250,7 +250,7 @@ public static ConsumeCodeResponse consumeCode(Main main, Storage storage = StorageLayer.getStorage(main); return consumeCode( new TenantIdentifierWithStorage(null, null, null, storage), - main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, false); + main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, false, true); } catch (TenantOrAppNotFoundException | BadPermissionException e) { throw new IllegalStateException(e); } @@ -267,7 +267,7 @@ public static ConsumeCodeResponse consumeCode(Main main, Storage storage = StorageLayer.getStorage(main); return consumeCode( new TenantIdentifierWithStorage(null, null, null, storage), - main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, setEmailVerified); + main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, setEmailVerified, true); } catch (TenantOrAppNotFoundException | BadPermissionException e) { throw new IllegalStateException(e); } @@ -282,12 +282,12 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException, TenantOrAppNotFoundException, BadPermissionException { return consumeCode(tenantIdentifierWithStorage, main, deviceId, deviceIdHashFromUser, userInputCode, linkCode, - false); + false, true); } public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String deviceId, String deviceIdHashFromUser, - String userInputCode, String linkCode, boolean setEmailVerified) + String userInputCode, String linkCode, boolean setEmailVerified, boolean createRecipeUserIfNotExists) throws RestartFlowException, ExpiredUserInputCodeException, IncorrectUserInputCodeException, DeviceIdHashMismatchException, StorageTransactionLogicException, StorageQueryException, NoSuchAlgorithmException, InvalidKeyException, IOException, Base64EncodingException, @@ -439,50 +439,52 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant } if (user == null) { - while (true) { - try { - String userId = Utils.getUUID(); - long timeJoined = System.currentTimeMillis(); - user = passwordlessStorage.createUser(tenantIdentifierWithStorage, userId, consumedDevice.email, - consumedDevice.phoneNumber, timeJoined); - - // Set email as verified, if using email - if (setEmailVerified && consumedDevice.email != null) { - try { - AuthRecipeUserInfo finalUser = user; - tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { - try { - tenantIdentifierWithStorage.getEmailVerificationStorage() - .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, - finalUser.getSupertokensUserId(), consumedDevice.email, true); - tenantIdentifierWithStorage.getEmailVerificationStorage() - .commitTransaction(con); - - return null; - } catch (TenantOrAppNotFoundException e) { - throw new StorageTransactionLogicException(e); + if (createRecipeUserIfNotExists) { + while (true) { + try { + String userId = Utils.getUUID(); + long timeJoined = System.currentTimeMillis(); + user = passwordlessStorage.createUser(tenantIdentifierWithStorage, userId, consumedDevice.email, + consumedDevice.phoneNumber, timeJoined); + + // Set email as verified, if using email + if (setEmailVerified && consumedDevice.email != null) { + try { + AuthRecipeUserInfo finalUser = user; + tenantIdentifierWithStorage.getEmailVerificationStorage().startTransaction(con -> { + try { + tenantIdentifierWithStorage.getEmailVerificationStorage() + .updateIsEmailVerified_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, + finalUser.getSupertokensUserId(), consumedDevice.email, true); + tenantIdentifierWithStorage.getEmailVerificationStorage() + .commitTransaction(con); + + return null; + } catch (TenantOrAppNotFoundException e) { + throw new StorageTransactionLogicException(e); + } + }); + user.loginMethods[0].setVerified(); // newly created user has only one loginMethod + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.actualException; } - }); - user.loginMethods[0].setVerified(); // newly created user has only one loginMethod - } catch (StorageTransactionLogicException e) { - if (e.actualException instanceof TenantOrAppNotFoundException) { - throw (TenantOrAppNotFoundException) e.actualException; + throw new StorageQueryException(e); } - throw new StorageQueryException(e); } - } - return new ConsumeCodeResponse(true, user, consumedDevice.email, consumedDevice.phoneNumber); - } catch (DuplicateEmailException | DuplicatePhoneNumberException e) { - // Getting these would mean that between getting the user and trying creating it: - // 1. the user managed to do a full create+consume flow - // 2. the users email or phoneNumber was updated to the new one (including device cleanup) - // These should be almost impossibly rare, so it's safe to just ask the user to restart. - // Also, both would make the current login fail if done before the transaction - // by cleaning up the device/code this consume would've used. - throw new RestartFlowException(); - } catch (DuplicateUserIdException e) { - // We can retry.. + 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 + // 2. the users email or phoneNumber was updated to the new one (including device cleanup) + // These should be almost impossibly rare, so it's safe to just ask the user to restart. + // Also, both would make the current login fail if done before the transaction + // by cleaning up the device/code this consume would've used. + throw new RestartFlowException(); + } catch (DuplicateUserIdException e) { + // We can retry.. + } } } } else { @@ -521,7 +523,7 @@ public static ConsumeCodeResponse consumeCode(TenantIdentifierWithStorage tenant removeCodesByPhoneNumber(tenantIdentifierWithStorage, loginMethod.phoneNumber); } } - return new ConsumeCodeResponse(false, user, consumedDevice.email, consumedDevice.phoneNumber); + return new ConsumeCodeResponse(false, user, consumedDevice.email, consumedDevice.phoneNumber, consumedDevice); } @TestOnly @@ -862,15 +864,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 35ee2fd60..eda332fcf 100644 --- a/src/main/java/io/supertokens/session/Session.java +++ b/src/main/java/io/supertokens/session/Session.java @@ -377,7 +377,7 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M accessToken.sessionHandle, Utils.hashSHA256(accessToken.refreshTokenHash1), System.currentTimeMillis() + - config.getRefreshTokenValidity()); + config.getRefreshTokenValidity(), sessionInfo.useStaticKey); } storage.commitTransaction(con); @@ -454,7 +454,7 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M Utils.hashSHA256(accessToken.refreshTokenHash1), System.currentTimeMillis() + Config.getConfig(tenantIdentifierWithStorage, main) .getRefreshTokenValidity(), - sessionInfo.lastUpdatedSign); + sessionInfo.lastUpdatedSign, sessionInfo.useStaticKey); if (!success) { continue; } @@ -509,7 +509,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); } @@ -518,7 +518,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 { @@ -534,14 +534,14 @@ public static SessionInformationHolder refreshSession(AppIdentifier appIdentifie return refreshSessionHelper(refreshTokenInfo.tenantIdentifier.withStorage( StorageLayer.getStorage(refreshTokenInfo.tenantIdentifier, main)), - main, refreshToken, refreshTokenInfo, enableAntiCsrf, accessTokenVersion); + main, refreshToken, refreshTokenInfo, enableAntiCsrf, accessTokenVersion, shouldUseStaticKey); } private static SessionInformationHolder refreshSessionHelper( TenantIdentifierWithStorage tenantIdentifierWithStorage, 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 { @@ -565,8 +565,16 @@ private static SessionInformationHolder refreshSessionHelper( storage.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 + storage.updateSessionInfo_Transaction(tenantIdentifierWithStorage, con, sessionHandle, + sessionInfo.refreshTokenHash2, sessionInfo.expiry, + useStaticKey); + } + // at this point, the input refresh token is the parent one. storage.commitTransaction(con); @@ -580,7 +588,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); @@ -600,13 +609,13 @@ private static SessionInformationHolder refreshSessionHelper( .equals(sessionInfo.refreshTokenHash2))) { storage.updateSessionInfo_Transaction(tenantIdentifierWithStorage, con, sessionHandle, Utils.hashSHA256(Utils.hashSHA256(refreshToken)), - System.currentTimeMillis() + config.getRefreshTokenValidity()); + System.currentTimeMillis() + config.getRefreshTokenValidity(), useStaticKey); storage.commitTransaction(con); return refreshSessionHelper(tenantIdentifierWithStorage, main, refreshToken, refreshTokenInfo, enableAntiCsrf, - accessTokenVersion); + accessTokenVersion, shouldUseStaticKey); } storage.commitTransaction(con); @@ -655,7 +664,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 = storage.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; @@ -666,7 +686,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); @@ -688,13 +709,12 @@ private static SessionInformationHolder refreshSessionHelper( Utils.hashSHA256(Utils.hashSHA256(refreshToken)), System.currentTimeMillis() + Config.getConfig(tenantIdentifierWithStorage, main).getRefreshTokenValidity(), - sessionInfo.lastUpdatedSign); + sessionInfo.lastUpdatedSign, useStaticKey); if (!success) { continue; } return refreshSessionHelper(tenantIdentifierWithStorage, main, refreshToken, refreshTokenInfo, - enableAntiCsrf, - accessTokenVersion); + 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 307f49471..a03a38fda 100644 --- a/src/main/java/io/supertokens/totp/Totp.java +++ b/src/main/java/io/supertokens/totp/Totp.java @@ -3,20 +3,18 @@ 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.mfa.Mfa; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; -import io.supertokens.pluginInterface.multitenancy.AppIdentifier; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; 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; @@ -70,23 +68,11 @@ 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 { + FeatureNotEnabledException, StorageTransactionLogicException { try { return registerDevice(new AppIdentifierWithStorage(null, null, StorageLayer.getStorage(main)), main, userId, deviceName, skew, period); @@ -95,30 +81,80 @@ public static TOTPDevice registerDevice(Main main, String userId, } } - public static TOTPDevice registerDevice(AppIdentifierWithStorage appIdentifierWithStorage, Main main, String userId, - String deviceName, int skew, int period) - throws StorageQueryException, DeviceAlreadyExistsException, NoSuchAlgorithmException, - FeatureNotEnabledException, TenantOrAppNotFoundException { + public static TOTPDevice createDevice(Main main, AppIdentifierWithStorage appIdentifierWithStorage, String userId, + String deviceName, int skew, int period, String secretKey, boolean verified, + long createdAt) + throws DeviceAlreadyExistsException, StorageQueryException, FeatureNotEnabledException, + TenantOrAppNotFoundException { - if (!isTotpEnabled(appIdentifierWithStorage, main)) { - throw new FeatureNotEnabledException( - "TOTP feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " + - "feature."); + Mfa.checkForMFAFeature(appIdentifierWithStorage, main); + + if (deviceName != null) { + TOTPSQLStorage totpStorage = appIdentifierWithStorage.getTOTPStorage(); + try { + return totpStorage.startTransaction(con -> { + try { + TOTPDevice existingDevice = totpStorage.getDeviceByName_Transaction(con, appIdentifierWithStorage, userId, deviceName); + if (existingDevice == null) { + return totpStorage.createDevice_Transaction(con, appIdentifierWithStorage, new TOTPDevice( + userId, deviceName, secretKey, period, skew, verified, createdAt + )); + } else if (!existingDevice.verified) { + totpStorage.deleteDevice_Transaction(con, appIdentifierWithStorage, userId, deviceName); + return totpStorage.createDevice_Transaction(con, appIdentifierWithStorage, 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 = appIdentifierWithStorage.getTOTPStorage(); + TOTPDevice[] devices = totpStorage.getDevices(appIdentifierWithStorage, userId); + int verifiedDevicesCount = Arrays.stream(devices).filter(d -> d.verified).toArray().length; + + while (true) { + try { + return createDevice(main, appIdentifierWithStorage, + userId, + "TOTP Device " + verifiedDevicesCount, + skew, + period, + secretKey, + verified, + createdAt + ); + } catch (DeviceAlreadyExistsException e){ + } + verifiedDevicesCount++; + } + } + + public static TOTPDevice registerDevice(AppIdentifierWithStorage appIdentifierWithStorage, Main main, String userId, + String deviceName, int skew, int period) + throws StorageQueryException, DeviceAlreadyExistsException, NoSuchAlgorithmException, + FeatureNotEnabledException, TenantOrAppNotFoundException, StorageTransactionLogicException { - String secret = generateSecret(); - TOTPDevice device = new TOTPDevice(userId, deviceName, secret, period, skew, false); - totpStorage.createDevice(appIdentifierWithStorage, device); + String secretKey = generateSecret(); - return device; + return createDevice(main, appIdentifierWithStorage, userId, deviceName, skew, period, secretKey, false, + System.currentTimeMillis()); } private static void checkAndStoreCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, 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 @@ -156,135 +192,126 @@ private static void checkAndStoreCode(TenantIdentifierWithStorage tenantIdentifi TOTPSQLStorage totpSQLStorage = tenantIdentifierWithStorage.getTOTPStorage(); - while (true) { - try { - totpSQLStorage.startTransaction(con -> { - try { - TOTPUsedCode[] usedCodes = totpSQLStorage.getAllUsedCodesDescOrder_Transaction(con, - tenantIdentifierWithStorage, - userId); - - // N represents # of invalid attempts that will trigger rate limiting: - int N = Config.getConfig(tenantIdentifierWithStorage, 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(tenantIdentifierWithStorage, 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, + tenantIdentifierWithStorage, + userId); + + // N represents # of invalid attempts that will trigger rate limiting: + int N = Config.getConfig(tenantIdentifierWithStorage, 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(tenantIdentifierWithStorage, 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, tenantIdentifierWithStorage, 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, tenantIdentifierWithStorage, 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 TenantIdentifierWithStorage(null, null, null, StorageLayer.getStorage(main)), main, @@ -295,8 +322,8 @@ public static boolean verifyDevice(Main main, } public static boolean verifyDevice(TenantIdentifierWithStorage tenantIdentifierWithStorage, 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 +339,7 @@ public static boolean verifyDevice(TenantIdentifierWithStorage tenantIdentifierW // Check if the user has any devices: TOTPDevice[] devices = totpStorage.getDevices(tenantIdentifierWithStorage.toAppIdentifier(), userId); if (devices.length == 0) { - throw new TotpNotEnabledException(); + throw new UnknownDeviceException(); } // Check if the requested device exists: @@ -337,62 +364,63 @@ public static boolean verifyDevice(TenantIdentifierWithStorage tenantIdentifierW // 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(tenantIdentifierWithStorage, main, userId, new TOTPDevice[]{matchingDevice}, - code); + try { + checkAndStoreCode(tenantIdentifierWithStorage, 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(tenantIdentifierWithStorage.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 TenantIdentifierWithStorage(null, null, null, StorageLayer.getStorage(main)), main, - userId, code, allowUnverifiedDevices); + userId, code); } catch (TenantOrAppNotFoundException e) { throw new IllegalStateException(e); } } - public static void verifyCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String userId, - String code, boolean allowUnverifiedDevices) - throws TotpNotEnabledException, InvalidTotpException, LimitReachedException, + public static void verifyCode(TenantIdentifierWithStorage tenantIdentifierWithStorage, Main main, String userId, String code) + throws InvalidTotpException, UnknownTotpUserIdException, LimitReachedException, StorageQueryException, StorageTransactionLogicException, FeatureNotEnabledException, TenantOrAppNotFoundException { - if (!isTotpEnabled(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main)) { - throw new FeatureNotEnabledException( - "TOTP feature is not enabled. Please subscribe to a SuperTokens core license key to enable this " + - "feature."); - } + Mfa.checkForMFAFeature(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main); TOTPSQLStorage totpStorage = tenantIdentifierWithStorage.getTOTPStorage(); // Check if the user has any devices: TOTPDevice[] devices = totpStorage.getDevices(tenantIdentifierWithStorage.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(tenantIdentifierWithStorage, 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 AppIdentifierWithStorage(null, null, StorageLayer.getStorage(main)), @@ -406,8 +434,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(AppIdentifierWithStorage appIdentifierWithStorage, String userId, - String deviceName) - throws StorageQueryException, UnknownDeviceException, TotpNotEnabledException, + String deviceName) + throws StorageQueryException, UnknownDeviceException, StorageTransactionLogicException, TenantOrAppNotFoundException { TOTPSQLStorage storage = appIdentifierWithStorage.getTOTPStorage(); @@ -432,12 +460,6 @@ public static void removeDevice(AppIdentifierWithStorage appIdentifierWithStorag return; } catch (StorageTransactionLogicException e) { if (e.actualException instanceof UnknownDeviceException) { - // Check if any device exists for the user: - TOTPDevice[] devices = storage.getDevices(appIdentifierWithStorage, userId); - if (devices.length == 0) { - throw new TotpNotEnabledException(); - } - throw (UnknownDeviceException) e.actualException; } @@ -447,9 +469,8 @@ public static void removeDevice(AppIdentifierWithStorage appIdentifierWithStorag @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 AppIdentifierWithStorage(null, null, StorageLayer.getStorage(main)), userId, oldDeviceName, newDeviceName); @@ -459,26 +480,16 @@ public static void updateDeviceName(Main main, String userId, } public static void updateDeviceName(AppIdentifierWithStorage appIdentifierWithStorage, String userId, - String oldDeviceName, String newDeviceName) + String oldDeviceName, String newDeviceName) throws StorageQueryException, DeviceAlreadyExistsException, UnknownDeviceException, - TotpNotEnabledException, TenantOrAppNotFoundException { + TenantOrAppNotFoundException { TOTPSQLStorage totpStorage = appIdentifierWithStorage.getTOTPStorage(); - try { - totpStorage.updateDeviceName(appIdentifierWithStorage, userId, oldDeviceName, newDeviceName); - } catch (UnknownDeviceException e) { - // Check if any device exists for the user: - TOTPDevice[] devices = totpStorage.getDevices(appIdentifierWithStorage, userId); - if (devices.length == 0) { - throw new TotpNotEnabledException(); - } else { - throw e; - } - } + totpStorage.updateDeviceName(appIdentifierWithStorage, userId, oldDeviceName, newDeviceName); } @TestOnly public static TOTPDevice[] getDevices(Main main, String userId) - throws StorageQueryException, TotpNotEnabledException { + throws StorageQueryException { try { return getDevices(new AppIdentifierWithStorage(null, null, StorageLayer.getStorage(main)), userId); @@ -488,13 +499,10 @@ public static TOTPDevice[] getDevices(Main main, String userId) } public static TOTPDevice[] getDevices(AppIdentifierWithStorage appIdentifierWithStorage, String userId) - throws StorageQueryException, TotpNotEnabledException, TenantOrAppNotFoundException { + throws StorageQueryException, TenantOrAppNotFoundException { TOTPSQLStorage totpStorage = appIdentifierWithStorage.getTOTPStorage(); TOTPDevice[] devices = totpStorage.getDevices(appIdentifierWithStorage, 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/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 78059bf13..d493eec32 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -218,6 +218,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 a87130a75..03919bc10 100644 --- a/src/main/java/io/supertokens/webserver/WebserverAPI.java +++ b/src/main/java/io/supertokens/webserver/WebserverAPI.java @@ -30,6 +30,7 @@ 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.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; @@ -77,10 +78,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) @@ -343,6 +345,15 @@ protected AppIdentifierWithStorage getAppIdentifierWithStorageFromRequestAndEnfo storage, storages); } + protected AppIdentifierWithStorage getPublicTenantStorage(HttpServletRequest req) + throws ServletException, TenantOrAppNotFoundException { + AppIdentifier appIdentifier = new AppIdentifier(this.getConnectionUriDomain(req), this.getAppId(req)); + + Storage storage = StorageLayer.getStorage(appIdentifier.getAsPublicTenantIdentifier(), main); + + return appIdentifier.withStorage(storage); + } + protected TenantIdentifierWithStorageAndUserIdMapping getTenantIdentifierWithStorageAndUserIdMappingFromRequest( HttpServletRequest req, String userId, UserIdType userIdType) throws StorageQueryException, TenantOrAppNotFoundException, UnknownUserIdException, ServletException { 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 cf57898cd..993950ea2 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; @@ -29,8 +30,6 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.pluginInterface.useridmapping.UserIdMapping; -import io.supertokens.useridmapping.UserIdType; import io.supertokens.utils.SemVer; import io.supertokens.utils.Utils; import io.supertokens.webserver.InputParser; @@ -79,7 +78,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I password); io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(tenantIdentifierWithStorage, new AuthRecipeUserInfo[]{user}); - ActiveUsers.updateLastActive(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, user.getSupertokensUserId()); // use the internal user id JsonObject result = new JsonObject(); 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 bb915fadb..da5725b77 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; @@ -80,7 +81,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I TenantIdentifierWithStorage tenant = this.getTenantIdentifierWithStorageFromRequest(req); AuthRecipeUserInfo user = EmailPassword.signUp(tenant, super.main, normalisedEmail, password); - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, user.getSupertokensUserId()); + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); @@ -97,6 +98,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/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 d84cfcf7b..c51e2f382 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateAppAPI.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/CreateOrUpdateAppAPI.java @@ -16,19 +16,21 @@ package io.supertokens.webserver.api.multitenancy; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.supertokens.Main; -import io.supertokens.multitenancy.exception.BadPermissionException; 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 io.supertokens.webserver.api.multitenancy.BaseCreateOrUpdate; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; public class CreateOrUpdateAppAPI extends BaseCreateOrUpdate { @@ -56,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 = this.getTenantIdentifierWithStorageFromRequest(req); @@ -66,7 +98,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 8d88cb8d6..c17643d29 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 = this.getTenantIdentifierWithStorageFromRequest(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 27d08cc40..d67a7b533 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 = this.getTenantIdentifierWithStorageFromRequest(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, tenantIdentifier.getStorage(), 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/thirdparty/CreateOrUpdateThirdPartyConfigAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/thirdparty/CreateOrUpdateThirdPartyConfigAPI.java index eff3a93d9..57f440390 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 8f5b5570c..7c086b2ef 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 @@ -82,7 +82,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/ConsumeCodeAPI.java b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java index 6329a81d2..404947121 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/ConsumeCodeAPI.java @@ -19,6 +19,7 @@ import com.google.gson.JsonObject; import io.supertokens.ActiveUsers; import io.supertokens.Main; +import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.passwordless.Passwordless; import io.supertokens.passwordless.Passwordless.ConsumeCodeResponse; @@ -29,8 +30,6 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.pluginInterface.useridmapping.UserIdMapping; -import io.supertokens.useridmapping.UserIdType; import io.supertokens.utils.SemVer; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; @@ -65,6 +64,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I String linkCode = null; String deviceId = null; String userInputCode = null; + Boolean createRecipeUserIfNotExists = true; String deviceIdHash = InputParser.parseStringOrThrowError(input, "preAuthSessionId", false); @@ -82,40 +82,70 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I new BadRequestException("Please provide exactly one of linkCode or deviceId+userInputCode")); } + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_0)) { + if (input.has("createRecipeUserIfNotExists")) { + createRecipeUserIfNotExists = InputParser.parseBooleanOrThrowError(input, "createRecipeUserIfNotExists", false); + } + } + try { ConsumeCodeResponse consumeCodeResponse = Passwordless.consumeCode( this.getTenantIdentifierWithStorageFromRequest(req), main, deviceId, deviceIdHash, userInputCode, linkCode, // From CDI version 4.0 onwards, the email verification will be set - getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)); + getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0), + createRecipeUserIfNotExists); + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{consumeCodeResponse.user}); - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, consumeCodeResponse.user.getSupertokensUserId()); + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, consumeCodeResponse.user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); - JsonObject userJson = - getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? consumeCodeResponse.user.toJson() : - consumeCodeResponse.user.toJsonWithoutAccountLinking(); - if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { - userJson.remove("tenantIds"); - } + if (consumeCodeResponse.user != null) { + io.supertokens.useridmapping.UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{consumeCodeResponse.user}); + + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, consumeCodeResponse.user.getSupertokensUserId()); + + JsonObject userJson = getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0) ? consumeCodeResponse.user.toJson() : + consumeCodeResponse.user.toJsonWithoutAccountLinking(); + + if (getVersionFromRequest(req).lesserThan(SemVer.v3_0)) { + userJson.remove("tenantIds"); + } - result.addProperty("createdNewUser", consumeCodeResponse.createdNewUser); - result.add("user", userJson); - if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { - for (LoginMethod loginMethod : consumeCodeResponse.user.loginMethods) { - if (loginMethod.recipeId.equals(RECIPE_ID.PASSWORDLESS) - && (consumeCodeResponse.email == null || Objects.equals(loginMethod.email, consumeCodeResponse.email)) - && (consumeCodeResponse.phoneNumber == null || Objects.equals(loginMethod.phoneNumber, consumeCodeResponse.phoneNumber))) { - result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId()); - break; + result.addProperty("createdNewUser", consumeCodeResponse.createdNewUser); + result.add("user", userJson); + if (getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v4_0)) { + for (LoginMethod loginMethod : consumeCodeResponse.user.loginMethods) { + if (loginMethod.recipeId.equals(RECIPE_ID.PASSWORDLESS) + && (consumeCodeResponse.email == null || Objects.equals(loginMethod.email, consumeCodeResponse.email)) + && (consumeCodeResponse.phoneNumber == null || Objects.equals(loginMethod.phoneNumber, consumeCodeResponse.phoneNumber))) { + result.addProperty("recipeUserId", loginMethod.getSupertokensOrExternalUserId()); + break; + } } } } + 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/session/RefreshSessionAPI.java b/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java index ee92da0bd..e875ce909 100644 --- a/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/RefreshSessionAPI.java @@ -60,11 +60,15 @@ 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; @@ -75,13 +79,14 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I throw new ServletException(e); } - SemVer version = super.getVersionFromRequest(req); try { AccessToken.VERSION accessTokenVersion = AccessToken.getAccessTokenVersionForCDI(version); SessionInformationHolder sessionInfo = Session.refreshSession(appIdentifierWithStorage, main, refreshToken, antiCsrfToken, - enableAntiCsrf, accessTokenVersion); + enableAntiCsrf, accessTokenVersion, + useDynamicSigningKey == null ? null : Boolean.FALSE.equals(useDynamicSigningKey) + ); if (StorageLayer.getStorage(this.getTenantIdentifierWithStorageFromRequest(req), main).getType() == STORAGE_TYPE.SQL) { @@ -90,10 +95,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I this.getAppIdentifierWithStorage(req), sessionInfo.session.userId, UserIdType.ANY); if (userIdMapping != null) { - ActiveUsers.updateLastActive(appIdentifierWithStorage, main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, userIdMapping.superTokensUserId); } else { - ActiveUsers.updateLastActive(appIdentifierWithStorage, main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, sessionInfo.session.userId); } } catch (StorageQueryException ignored) { diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java index 4263285c2..7691ee81e 100644 --- a/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/SessionAPI.java @@ -110,10 +110,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I this.getAppIdentifierWithStorage(req), sessionInfo.session.userId, UserIdType.ANY); if (userIdMapping != null) { - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, userIdMapping.superTokensUserId); } else { - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, sessionInfo.session.userId); } } catch (StorageQueryException ignored) { diff --git a/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java b/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java index 127e0b5fc..22fba74cf 100644 --- a/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java +++ b/src/main/java/io/supertokens/webserver/api/session/SessionRemoveAPI.java @@ -116,10 +116,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I this.getAppIdentifierWithStorage(req), userId, UserIdType.ANY); if (userIdMapping != null) { - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, userIdMapping.superTokensUserId); } else { - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, userId); + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, userId); } } catch (StorageQueryException ignored) { } 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 06042eb23..5f50557ad 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.authRecipe.AuthRecipeUserInfo; @@ -28,7 +29,6 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.thirdparty.ThirdParty; import io.supertokens.useridmapping.UserIdMapping; -import io.supertokens.useridmapping.UserIdType; import io.supertokens.utils.SemVer; import io.supertokens.utils.Utils; import io.supertokens.webserver.InputParser; @@ -81,7 +81,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I thirdPartyUserId, email, isEmailVerified); UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{response.user}); - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.getSupertokensUserId()); + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, response.user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); @@ -103,6 +103,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } } } + super.sendJsonResponse(200, result, resp); } catch (StorageQueryException | TenantOrAppNotFoundException e) { @@ -139,7 +140,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I email, isEmailVerified); UserIdMapping.populateExternalUserIdForUsers(this.getTenantIdentifierWithStorageFromRequest(req), new AuthRecipeUserInfo[]{response.user}); - ActiveUsers.updateLastActive(this.getAppIdentifierWithStorage(req), main, response.user.getSupertokensUserId()); + ActiveUsers.updateLastActive(this.getPublicTenantStorage(req), main, response.user.getSupertokensUserId()); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); @@ -163,6 +164,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/totp/CreateOrUpdateTotpDeviceAPI.java b/src/main/java/io/supertokens/webserver/api/totp/CreateOrUpdateTotpDeviceAPI.java index 3d1ad47aa..01bc2c28a 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/CreateOrUpdateTotpDeviceAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/CreateOrUpdateTotpDeviceAPI.java @@ -7,11 +7,11 @@ import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.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; @@ -42,7 +42,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); @@ -52,7 +52,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) { @@ -87,13 +88,14 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I TOTPDevice device = Totp.registerDevice(appIdentifierWithStorage, 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) { result.addProperty("status", "DEVICE_ALREADY_EXISTS_ERROR"); super.sendJsonResponse(200, result, resp); } catch (StorageQueryException | NoSuchAlgorithmException | FeatureNotEnabledException | - TenantOrAppNotFoundException e) { + TenantOrAppNotFoundException | StorageTransactionLogicException e) { throw new ServletException(e); } } @@ -143,9 +145,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 079daae94..98da43d5c 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,6 @@ import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; 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; @@ -80,9 +79,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 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..3fc6b3621 --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/totp/ImportTotpDeviceAPI.java @@ -0,0 +1,106 @@ +package io.supertokens.webserver.api.totp; + +import com.google.gson.JsonObject; +import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; +import io.supertokens.Main; +import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; +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.UnknownDeviceException; +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; +import java.security.NoSuchAlgorithmException; + +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 { + AppIdentifierWithStorage appIdentifierWithStorage; + 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. + + // Try to find the appIdentifier with right storage based on the userId + AppIdentifierWithStorageAndUserIdMapping mappingAndStorage = + getAppIdentifierWithStorageAndUserIdMappingFromRequest( + req, userId, UserIdType.ANY); + + if (mappingAndStorage.userIdMapping != null) { + userId = mappingAndStorage.userIdMapping.superTokensUserId; + } + appIdentifierWithStorage = mappingAndStorage.appIdentifierWithStorage; + } catch (UnknownUserIdException e) { + // if the user is not found, just use the storage of the tenant of interest + appIdentifierWithStorage = getAppIdentifierWithStorage(req); + } + + TOTPDevice createdDevice = Totp.createDevice(super.main, appIdentifierWithStorage, + 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 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 df3ea4801..d6b0c6f50 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/RemoveTotpDeviceAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/RemoveTotpDeviceAPI.java @@ -9,7 +9,6 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; 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; @@ -75,9 +74,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 7d3980d99..07a1df325 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpAPI.java @@ -4,7 +4,6 @@ import com.google.gson.JsonObject; -import io.supertokens.AppIdentifierWithStorageAndUserIdMapping; import io.supertokens.Main; import io.supertokens.TenantIdentifierWithStorageAndUserIdMapping; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; @@ -12,15 +11,14 @@ import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; -import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -import io.supertokens.pluginInterface.totp.exception.TotpNotEnabledException; -import io.supertokens.pluginInterface.useridmapping.UserIdMapping; +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.useridmapping.UserIdType; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -46,7 +44,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")); @@ -54,7 +51,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(); @@ -76,19 +72,27 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I tenantIdentifierWithStorage = getTenantIdentifierWithStorageFromRequest(req); } - Totp.verifyCode(tenantIdentifierWithStorage, main, userId, totp, allowUnverifiedDevices); + Totp.verifyCode(tenantIdentifierWithStorage, 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 a068e07e4..0cdfe0a8c 100644 --- a/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpDeviceAPI.java +++ b/src/main/java/io/supertokens/webserver/api/totp/VerifyTotpDeviceAPI.java @@ -12,13 +12,12 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; -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.utils.SemVer; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -80,18 +79,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/ActiveUsersTest.java b/src/test/java/io/supertokens/test/ActiveUsersTest.java index c88a5bbef..108bc38ee 100644 --- a/src/test/java/io/supertokens/test/ActiveUsersTest.java +++ b/src/test/java/io/supertokens/test/ActiveUsersTest.java @@ -4,10 +4,15 @@ import io.supertokens.ActiveUsers; import io.supertokens.Main; import io.supertokens.ProcessState; +import io.supertokens.featureflag.EE_FEATURES; +import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.httpRequest.HttpRequestForTesting; import io.supertokens.test.httpRequest.HttpResponseException; +import io.supertokens.test.multitenant.api.TestMultitenancyAPIHelper; +import io.supertokens.utils.SemVer; import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; @@ -212,4 +217,80 @@ public void activeUserCountAPITest() throws Exception { assert res.get("count").getAsInt() == 2; } + @Test + public void testThatActiveUserDataIsSavedInPublicTenantStorage() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { // Create a tenant + JsonObject coreConfig = new JsonObject(); + + StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess()) + .modifyConfigToAddANewUserPoolForTesting(coreConfig, 1); + + TestMultitenancyAPIHelper.createTenant( + process.getProcess(), + new TenantIdentifier(null, null, null), + "t1", true, true, true, + coreConfig); + } + + { // no active users yet + HashMap params = new HashMap<>(); + params.put("since", "0"); + JsonObject res = HttpRequestForTesting.sendGETRequest( + process.getProcess(), + "", + "http://localhost:3567/users/count/active", + params, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + ""); + + assert res.get("status").getAsString().equals("OK"); + assert res.get("count").getAsInt() == 0; + } + + { // Sign up, which updates active users + JsonObject responseBody = new JsonObject(); + responseBody.addProperty("email", "random@gmail.com"); + responseBody.addProperty("password", "validPass123"); + + JsonObject signInResponse = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/t1/recipe/signup", responseBody, 1000, 1000, null, SemVer.v4_0.get(), + "emailpassword"); + } + + { // 1 active user in the public tenant + HashMap params = new HashMap<>(); + params.put("since", "0"); + JsonObject res = HttpRequestForTesting.sendGETRequest( + process.getProcess(), + "", + "http://localhost:3567/users/count/active", + params, + 1000, + 1000, + null, + Utils.getCdiVersionStringLatestForTests(), + ""); + + assert res.get("status").getAsString().equals("OK"); + assert res.get("count").getAsInt() == 1; + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } } 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 f0766bac5..408351098 100644 --- a/src/test/java/io/supertokens/test/FeatureFlagTest.java +++ b/src/test/java/io/supertokens/test/FeatureFlagTest.java @@ -21,10 +21,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import io.supertokens.ProcessState; -import io.supertokens.cronjobs.CronTask; -import io.supertokens.cronjobs.CronTaskTest; -import io.supertokens.cronjobs.Cronjobs; -import io.supertokens.cronjobs.syncCoreConfigWithDb.SyncCoreConfigWithDb; +import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlag; @@ -32,7 +29,7 @@ import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.featureflag.exceptions.NoLicenseKeyFoundException; import io.supertokens.multitenancy.Multitenancy; -import io.supertokens.multitenancy.MultitenancyHelper; +import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.STORAGE_TYPE; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.exceptions.StorageQueryException; @@ -97,7 +94,7 @@ public void noLicenseKeyShouldHaveEmptyFeatureFlag() JsonObject stats = FeatureFlag.getInstance(process.getProcess()).getPaidFeatureStats(); Assert.assertEquals(stats.entrySet().size(), 1); - Assert.assertEquals(stats.get("maus").getAsJsonArray().size(), 30); + Assert.assertEquals(stats.get("maus").getAsJsonArray().size(), 31); Assert.assertEquals(stats.get("maus").getAsJsonArray().get(0).getAsInt(), 0); Assert.assertEquals(stats.get("maus").getAsJsonArray().get(29).getAsInt(), 0); @@ -159,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); @@ -173,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 { @@ -189,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 maus.size() == 30; + 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() == 30; - 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. @@ -215,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: { @@ -247,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 maus.size() == 30; + 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 totpMaus.size() == 30; - assert totpMaus.get(0).getAsInt() == 1; // only 1 user has TOTP enabled - assert totpMaus.get(29).getAsInt() == 1; + assert mfaMaus.size() == 31; + assert mfaMaus.get(0).getAsInt() == 1; // only 1 user has TOTP enabled + assert mfaMaus.get(29).getAsInt() == 1; - assert totalTotpUsers == 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() == 30; + 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()); + + 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() == 30; + assert mfaMaus.get(0).getAsInt() == 2; // 1 TOTP user + 1 account linked user + assert mfaMaus.get(29).getAsInt() == 2; + + 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").withStorage(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() == 30; + 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"; @@ -297,6 +422,7 @@ public void testFeatureFlagWithMultitenancyFor500Tenants() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ) ); @@ -358,6 +484,7 @@ public void testThatMultitenantStatsAreAccurate() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -385,6 +512,7 @@ public void testThatMultitenantStatsAreAccurate() throws Exception { null, null, null, null, null, null, null) }), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -457,6 +585,7 @@ public void testThatMultitenantStatsAreAccurateForAnApp() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ) ); @@ -475,6 +604,7 @@ public void testThatMultitenantStatsAreAccurateForAnApp() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -502,6 +632,7 @@ public void testThatMultitenantStatsAreAccurateForAnApp() throws Exception { null, null, null, null, null, null, null) }), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -583,6 +714,7 @@ public void testThatMultitenantStatsAreAccurateForACud() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -602,6 +734,7 @@ public void testThatMultitenantStatsAreAccurateForACud() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -629,6 +762,7 @@ public void testThatMultitenantStatsAreAccurateForACud() throws Exception { null, null, null, null, null, null, null) }), new PasswordlessConfig(true), + null, null, coreConfig ) ); @@ -701,6 +835,7 @@ public void testPaidFeaturesAreEnabledIfUsingInMemoryDatabase() throws Exception new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, new JsonObject() ) ); @@ -754,6 +889,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 = {"../"}; @@ -764,7 +902,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 e72daae9b..b58d80664 100644 --- a/src/test/java/io/supertokens/test/PathRouterTest.java +++ b/src/test/java/io/supertokens/test/PathRouterTest.java @@ -91,7 +91,7 @@ public void basicTenantIdFetchingTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -102,7 +102,7 @@ public void basicTenantIdFetchingTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -113,7 +113,7 @@ public void basicTenantIdFetchingTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -263,7 +263,7 @@ public void basicTenantIdFetchingWihQueryParamTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -274,7 +274,7 @@ public void basicTenantIdFetchingWihQueryParamTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -285,7 +285,7 @@ public void basicTenantIdFetchingWihQueryParamTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -435,7 +435,7 @@ public void basicTenantIdFetchingWithBasePathTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -446,7 +446,7 @@ public void basicTenantIdFetchingWithBasePathTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -457,7 +457,7 @@ public void basicTenantIdFetchingWithBasePathTest() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -617,7 +617,7 @@ public void basicTenantIdFetchingWithBasePathTest2() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -628,7 +628,7 @@ public void basicTenantIdFetchingWithBasePathTest2() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -639,7 +639,7 @@ public void basicTenantIdFetchingWithBasePathTest2() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -800,7 +800,7 @@ public void basicTenantIdFetchingWithBasePathTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -811,7 +811,7 @@ public void basicTenantIdFetchingWithBasePathTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -822,7 +822,7 @@ public void basicTenantIdFetchingWithBasePathTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -980,7 +980,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( @@ -990,7 +990,7 @@ public void withRecipeRouterTest() throws Exception { new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false); @@ -1322,7 +1322,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( @@ -1330,7 +1330,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( @@ -1338,7 +1338,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( @@ -1346,7 +1346,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 ); @@ -1443,7 +1443,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( @@ -1451,7 +1451,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( @@ -1459,7 +1459,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 ); @@ -1565,7 +1565,7 @@ public void tenantNotFoundTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -1575,7 +1575,7 @@ public void tenantNotFoundTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); @@ -1636,7 +1636,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1647,7 +1647,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1658,7 +1658,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1669,7 +1669,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1680,7 +1680,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1691,7 +1691,7 @@ public void basicAppIdTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), 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 ); @@ -1937,7 +1937,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1948,7 +1948,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1959,7 +1959,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1970,7 +1970,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1981,7 +1981,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -1992,7 +1992,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2003,7 +2003,7 @@ public void basicAppIdWithBasePathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2222,7 +2222,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2233,7 +2233,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2244,7 +2244,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2255,7 +2255,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2266,7 +2266,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2277,7 +2277,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2288,7 +2288,7 @@ public void basicAppIdWithBase2PathTesting() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(false), - new JsonObject() + null, null, new JsonObject() ), false ); @@ -2524,7 +2524,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( @@ -2532,7 +2532,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( @@ -2540,7 +2540,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( @@ -2548,7 +2548,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(), "") { @@ -2598,7 +2598,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( @@ -2606,7 +2606,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 ); @@ -2664,7 +2664,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( @@ -2672,7 +2672,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( @@ -2680,7 +2680,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( @@ -2688,7 +2688,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( @@ -2696,7 +2696,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 ); @@ -2833,7 +2833,7 @@ public void tenantNotFoundWithAppIdTest3() new EmailPasswordConfig(false), new ThirdPartyConfig(false, new ThirdPartyConfig.Provider[0]), new PasswordlessConfig(false), - tenantConfig), + null, null, tenantConfig), false ); Multitenancy.addNewOrUpdateAppOrTenant( @@ -2843,7 +2843,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 ec49fd89b..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 @@ -94,12 +97,11 @@ public void totpCodeLengthTest() throws Exception { // This error will be different in Postgres and MySQL // We added (CHECK (LENGTH(code) <= 8)) to the table definition in SQLite String totpUsedCodeTable = Config.getConfig(start).getTotpUsedCodesTable(); - assert e.getMessage().contains("CHECK constraint failed: " + totpUsedCodeTable); + assert e.getMessage().contains("CHECK constraint failed: "); } // Try code with length < 8 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/TelemetryTest.java b/src/test/java/io/supertokens/test/TelemetryTest.java index 95e312ac7..968d154f4 100644 --- a/src/test/java/io/supertokens/test/TelemetryTest.java +++ b/src/test/java/io/supertokens/test/TelemetryTest.java @@ -21,7 +21,10 @@ import io.supertokens.ProcessState; import io.supertokens.ProcessState.PROCESS_STATE; import io.supertokens.cronjobs.telemetry.Telemetry; +import io.supertokens.dashboard.Dashboard; import io.supertokens.httpRequest.HttpRequestMocking; +import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager.TestingProcess; import io.supertokens.version.Version; import org.junit.AfterClass; @@ -111,6 +114,16 @@ public void testThatTelemetryWorks() throws Exception { String[] args = { "../" }; TestingProcess process = TestingProcessManager.start(args, false); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getBaseStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) { + Dashboard.signUpDashboardUser(process.getProcess(), "test@example.com", "password123"); + } + + // Restarting the process to send telemetry again + process.kill(false); + process = TestingProcessManager.start(args, false); ByteArrayOutputStream output = new ByteArrayOutputStream(); final HttpURLConnection mockCon = mock(HttpURLConnection.class); @@ -149,13 +162,26 @@ protected URLConnection openConnection(URL u) { assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.SENT_TELEMETRY)); JsonObject telemetryData = new JsonParser().parse(output.toString()).getAsJsonObject(); + assertEquals(7, telemetryData.entrySet().size()); assertTrue(telemetryData.has("telemetryId")); assertEquals(telemetryData.get("superTokensVersion").getAsString(), Version.getVersion(process.getProcess()).getCoreVersion()); assertEquals(telemetryData.get("appId").getAsString(), "public"); assertEquals(telemetryData.get("connectionUriDomain").getAsString(), ""); - assertTrue(telemetryData.has("mau")); + assertTrue(telemetryData.has("maus")); + assertTrue(telemetryData.has("dashboardUserEmails")); + + if (StorageLayer.getBaseStorage(process.getProcess()).getType() == STORAGE_TYPE.SQL) { + assertEquals(1, telemetryData.get("dashboardUserEmails").getAsJsonArray().size()); + assertEquals("test@example.com", telemetryData.get("dashboardUserEmails").getAsJsonArray().get(0).getAsString()); + assertEquals(31, telemetryData.get("maus").getAsJsonArray().size()); + assertEquals(0, telemetryData.get("usersCount").getAsInt()); + } else { + assertEquals(0, telemetryData.get("dashboardUserEmails").getAsJsonArray().size()); + assertEquals(0, telemetryData.get("maus").getAsJsonArray().size()); + assertEquals(-1, telemetryData.get("usersCount").getAsInt()); + } process.kill(); assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STOPPED)); 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 5614de91d..9e3c23661 100644 --- a/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/CreatePrimaryUserTest.java @@ -422,7 +422,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())); TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", StorageLayer.getStorage(process.main)); @@ -468,7 +468,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())); TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", StorageLayer.getStorage(process.main)); diff --git a/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java b/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java index aab79a23a..8f4be10fe 100644 --- a/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java @@ -465,7 +465,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())); TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", StorageLayer.getStorage(process.main)); @@ -518,7 +518,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())); TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", StorageLayer.getStorage(process.main)); diff --git a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java index 438aea250..c4e463db4 100644 --- a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java @@ -99,7 +99,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -119,7 +119,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -139,7 +139,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -159,7 +159,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 8938c2700..70a666e70 100644 --- a/src/test/java/io/supertokens/test/accountlinking/SessionTests.java +++ b/src/test/java/io/supertokens/test/accountlinking/SessionTests.java @@ -89,7 +89,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -109,7 +109,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -129,7 +129,7 @@ private void createTenants(Main main) new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -149,7 +149,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/api/CreatePrimaryUserAPITest.java b/src/test/java/io/supertokens/test/accountlinking/api/CreatePrimaryUserAPITest.java index ce4dd6010..50be6b5d8 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 c2e58abfd..36a0c9f32 100644 --- a/src/test/java/io/supertokens/test/authRecipe/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/authRecipe/MultitenantAPITest.java @@ -122,6 +122,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -142,6 +143,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -162,6 +164,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 0e7f5e8d5..899da3906 100644 --- a/src/test/java/io/supertokens/test/authRecipe/UserPaginationTest.java +++ b/src/test/java/io/supertokens/test/authRecipe/UserPaginationTest.java @@ -120,6 +120,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -140,6 +141,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -160,6 +162,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/DashboardTest.java b/src/test/java/io/supertokens/test/dashboard/DashboardTest.java index 7bb9211a4..eeaa3b447 100644 --- a/src/test/java/io/supertokens/test/dashboard/DashboardTest.java +++ b/src/test/java/io/supertokens/test/dashboard/DashboardTest.java @@ -290,7 +290,7 @@ public void testDashboardUsageStats() throws Exception { JsonObject usageStats = response.get("usageStats").getAsJsonObject(); JsonArray mauArr = usageStats.get("maus").getAsJsonArray(); assertEquals(1, usageStats.entrySet().size()); - assertEquals(30, mauArr.size()); + assertEquals(31, mauArr.size()); assertEquals(0, mauArr.get(0).getAsInt()); assertEquals(0, mauArr.get(29).getAsInt()); } @@ -312,7 +312,7 @@ public void testDashboardUsageStats() throws Exception { JsonObject usageStats = response.get("usageStats").getAsJsonObject(); JsonArray mauArr = usageStats.get("maus").getAsJsonArray(); assertEquals(1, usageStats.entrySet().size()); - assertEquals(30, mauArr.size()); + assertEquals(31, mauArr.size()); assertEquals(0, mauArr.get(0).getAsInt()); assertEquals(0, mauArr.get(29).getAsInt()); } @@ -338,7 +338,7 @@ public void testDashboardUsageStats() throws Exception { JsonObject usageStats = response.get("usageStats").getAsJsonObject(); JsonObject dashboardLoginObject = usageStats.get("dashboard_login").getAsJsonObject(); assertEquals(2, usageStats.entrySet().size()); - assertEquals(30, usageStats.get("maus").getAsJsonArray().size()); + assertEquals(31, usageStats.get("maus").getAsJsonArray().size()); assertEquals(1, dashboardLoginObject.entrySet().size()); assertEquals(1, dashboardLoginObject.get("user_count").getAsInt()); } @@ -366,7 +366,7 @@ public void testDashboardUsageStats() throws Exception { JsonObject usageStats = response.get("usageStats").getAsJsonObject(); JsonObject dashboardLoginObject = usageStats.get("dashboard_login").getAsJsonObject(); assertEquals(2, usageStats.entrySet().size()); - assertEquals(30, usageStats.get("maus").getAsJsonArray().size()); + assertEquals(31, usageStats.get("maus").getAsJsonArray().size()); assertEquals(1, dashboardLoginObject.entrySet().size()); assertEquals(4, dashboardLoginObject.get("user_count").getAsInt()); } 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 a8dab3316..801652cc1 100644 --- a/src/test/java/io/supertokens/test/dashboard/apis/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/dashboard/apis/MultitenantAPITest.java @@ -109,6 +109,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -129,6 +130,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -149,6 +151,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 b9f9387bc..02e5df94a 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())); TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", @@ -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())); TenantIdentifierWithStorage tenantIdentifierWithStorage = new TenantIdentifierWithStorage(null, null, "t1", diff --git a/src/test/java/io/supertokens/test/emailpassword/MultitenantEmailPasswordTest.java b/src/test/java/io/supertokens/test/emailpassword/MultitenantEmailPasswordTest.java index 80a6fbfe1..fde572254 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 cc4ff6fdf..30735bc4e 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 2fc093038..d832b3573 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..83fe4e834 --- /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.withStorage(StorageLayer.getStorage(tenantIdentifier, process.main)), + process.getProcess(), "test@example.com", "abcd1234"); + + JsonObject userObj; + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertFalse(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + + // check user object + JsonObject jsonUser = response.get("user").getAsJsonObject(); + assert (jsonUser.get("id").getAsString().equals(user.getSupertokensUserId())); + assert (jsonUser.get("tenantIds").getAsJsonArray().size() == 1); + assert (jsonUser.get("tenantIds").getAsJsonArray().get(0).getAsString().equals("t1")); + assert (jsonUser.get("timeJoined").getAsLong() == user.timeJoined); + assert (jsonUser.get("isPrimaryUser").getAsBoolean()); + assert (jsonUser.get("emails").getAsJsonArray().size() == 1); + assert (jsonUser.get("emails").getAsJsonArray().get(0).getAsString().equals("test@example.com")); + assert (jsonUser.get("phoneNumbers").getAsJsonArray().size() == 0); + assert (jsonUser.get("thirdParty").getAsJsonArray().size() == 0); + assert (jsonUser.get("loginMethods").getAsJsonArray().size() == 1); + JsonObject lM = jsonUser.get("loginMethods").getAsJsonArray().get(0).getAsJsonObject(); + assertFalse(lM.get("verified").getAsBoolean()); + assert (lM.get("tenantIds").getAsJsonArray().size() == 1); + assert (lM.get("tenantIds").getAsJsonArray().get(0).getAsString().equals("t1")); + assertEquals(lM.get("timeJoined").getAsLong(), user.timeJoined); + assertEquals(lM.get("recipeUserId").getAsString(), user.getSupertokensUserId()); + assertEquals(lM.get("recipeId").getAsString(), "emailpassword"); + assertEquals(lM.get("email").getAsString(), "test@example.com"); + assert (lM.entrySet().size() == 6); + userObj = jsonUser; + } + + AuthRecipe.createPrimaryUser(process.main, + tenantIdentifier.toAppIdentifier().withStorage(StorageLayer.getStorage(tenantIdentifier, process.main)), + user.getSupertokensUserId()); + + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assertEquals(3, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + assertTrue(response.get("wasAlreadyAPrimaryUser").getAsBoolean()); + assertEquals(response.get("user"), userObj); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void createReturnsFailsWithoutFeatureEnabled() throws Exception { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + if (StorageLayer.isInMemDb(process.getProcess())) { + return; + } + + AuthRecipeUserInfo user = EmailPassword.signUp(process.getProcess(), "test@example.com", "abcd1234"); + + JsonObject userObj; + { + JsonObject params = new JsonObject(); + params.addProperty("recipeUserId", user.getSupertokensUserId()); + + try { + HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/accountlinking/user/primary", params, 1000, 1000, null, + WebserverAPI.getLatestCDIVersion().get(), ""); + assert (false); + } catch (HttpResponseException e) { + assertEquals(402, e.statusCode); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + } +} diff --git a/src/test/java/io/supertokens/test/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 b69b90bf5..e9a1df208 100644 --- a/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java +++ b/src/test/java/io/supertokens/test/multitenant/AppTenantUserTest.java @@ -66,7 +66,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)); @@ -105,7 +105,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( @@ -113,7 +113,7 @@ public void testDeletingAppDeleteNonAuthRecipeData() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); TenantIdentifierWithStorage tWithStorage = t.withStorage( @@ -145,7 +145,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( @@ -153,7 +153,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(tWithStorage.toAppIdentifierWithStorage(), @@ -171,7 +171,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)); @@ -202,7 +202,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( @@ -210,7 +210,7 @@ public void testDisassociationOfUserDeletesNonAuthRecipeData() throws Exception new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); TenantIdentifierWithStorage appWithStorage = app.withStorage( @@ -258,7 +258,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)); @@ -275,7 +275,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( @@ -283,7 +283,7 @@ public void deletingTenantKeepsTheUserInTheApp() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); TenantIdentifierWithStorage appWithStorage = app.withStorage( 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 c222b64c1..f995af0d4 100644 --- a/src/test/java/io/supertokens/test/multitenant/RequestConnectionUriDomainTest.java +++ b/src/test/java/io/supertokens/test/multitenant/RequestConnectionUriDomainTest.java @@ -19,7 +19,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import io.supertokens.ProcessState; -import io.supertokens.config.Config; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; @@ -37,7 +36,6 @@ import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; import io.supertokens.test.httpRequest.HttpRequestForTesting; -import io.supertokens.test.multitenant.api.TestMultitenancyAPIHelper; import io.supertokens.thirdparty.InvalidProviderConfigException; import io.supertokens.webserver.Webserver; import io.supertokens.webserver.WebserverAPI; @@ -145,11 +143,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(), "") { @@ -249,7 +247,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( @@ -257,7 +255,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( @@ -265,7 +263,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( @@ -273,7 +271,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 f6c4c679e..45194ebc6 100644 --- a/src/test/java/io/supertokens/test/multitenant/TestAppData.java +++ b/src/test/java/io/supertokens/test/multitenant/TestAppData.java @@ -95,7 +95,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)); @@ -103,7 +103,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); @@ -112,7 +112,7 @@ public void testThatDeletingAppDeleteDataFromAllTables() throws Exception { new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - new JsonObject() + null, null, new JsonObject() ), false); TenantIdentifierWithStorage appWithStorage = app.withStorage( @@ -154,8 +154,10 @@ public void testThatDeletingAppDeleteDataFromAllTables() throws Exception { TOTPDevice totpDevice = Totp.registerDevice(appWithStorage.toAppIdentifierWithStorage(), process.getProcess(), epUser.getSupertokensUserId(), "test", 1, 3); + Totp.verifyDevice(appWithStorage, process.getProcess(), epUser.getSupertokensUserId(), totpDevice.deviceName, + generateTotpCode(process.getProcess(), totpDevice, -1)); Totp.verifyCode(appWithStorage, process.getProcess(), epUser.getSupertokensUserId(), - generateTotpCode(process.getProcess(), totpDevice, 0), true); + generateTotpCode(process.getProcess(), totpDevice, 0)); ActiveUsers.updateLastActive(appWithStorage.toAppIdentifierWithStorage(), 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 b548ebb4c..36b6fabb9 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestApp.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestApp.java @@ -16,10 +16,10 @@ 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; -import io.supertokens.config.CoreConfigTestContent; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; @@ -37,6 +37,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 +49,7 @@ import org.junit.Test; import java.io.IOException; +import java.util.Set; import static org.junit.Assert.*; @@ -507,106 +509,529 @@ public void testDefaultRecipesEnabledWhileCreatingApp() throws Exception { } @Test - public void testInvalidTypedValueInCoreConfigWhileCreatingApp() throws Exception { + public void testFirstFactorsArray() throws Exception { if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; } - if (StorageLayer.isInMemDb(process.getProcess())) { + 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; } - String[] properties = new String[]{ - "access_token_validity", // long - "access_token_validity", // long - "access_token_validity", // long - "access_token_validity", // long - "disable_telemetry", // boolean - "postgresql_connection_pool_size", // int - "mysql_connection_pool_size", // int - }; - Object[] values = new Object[]{ - "abcd", // access_token_validity - "", - "null", - null, - "abcd", // disable_telemetry - "abcd", // postgresql_connection_pool_size - "abcd", // mysql_connection_pool_size - }; - - String[] expectedErrorMessages = new String[]{ - "Http error. Status Code: 400. Message: Invalid core config: 'access_token_validity' must be of type long", // access_token_validity - "Http error. Status Code: 400. Message: Invalid core config: 'access_token_validity' must be of type long", // access_token_validity - "Http error. Status Code: 400. Message: Invalid core config: 'access_token_validity' must be of type long", // access_token_validity - null, - "Http error. Status Code: 400. Message: Invalid core config: 'disable_telemetry' must be of type boolean", // disable_telemetry - "Http error. Status Code: 400. Message: Invalid core config: 'postgresql_connection_pool_size' must be of type int", // postgresql_connection_pool_size - "Http error. Status Code: 400. Message: Invalid core config: 'mysql_connection_pool_size' must be of type int", // mysql_connection_pool_size - }; - - System.out.println(StorageLayer.getStorage(process.getProcess()).getClass().getCanonicalName()); - - for (int i = 0; i < properties.length; i++) { + 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 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 { - System.out.println("Test case " + i); - JsonObject config = new JsonObject(); - if (values[i] == null) { - config.add(properties[i], null); - } - else if (values[i] instanceof String) { - config.addProperty(properties[i], (String) values[i]); - } else if (values[i] instanceof Boolean) { - config.addProperty(properties[i], (Boolean) values[i]); - } else if (values[i] instanceof Number) { - config.addProperty(properties[i], (Number) values[i]); - } else { - throw new RuntimeException("Invalid type"); - } - StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + 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()); + } - JsonObject response = TestMultitenancyAPIHelper.createApp( + { + 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, - config); - if (expectedErrorMessages[i] != null) { - fail(); - } + true, factors, false, null, + config, SemVer.v5_0); + fail(); } catch (HttpResponseException e) { assertEquals(400, e.statusCode); - if (!e.getMessage().contains("Invalid config key")) { - assertEquals(expectedErrorMessages[i], e.getMessage()); - } + 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 testInvalidCoreConfig() throws Exception { + public void testRequiredSecondaryFactorArrayValueValidationBasedOnDisabledRecipe() throws Exception { if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; } - CoreConfigTestContent.getInstance(process.getProcess()).setKeyValue(CoreConfigTestContent.VALIDITY_TESTING, - true); + + 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); { - JsonObject config = new JsonObject(); - config.addProperty("access_token_validity", 3600); - config.addProperty("refresh_token_validity", 3); - StorageLayer.getBaseStorage(process.getProcess()).modifyConfigToAddANewUserPoolForTesting(config, 1); + 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 { - JsonObject response = TestMultitenancyAPIHelper.createApp( + TestMultitenancyAPIHelper.createApp( process.getProcess(), new TenantIdentifier(null, null, null), "a1", null, null, null, - config); + 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: 'refresh_token_validity' must be strictly greater than 'access_token_validity'.", e.getMessage()); + 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()); + } + } + } } 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 8ca0855f1..75ffcf498 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.*; @@ -487,4 +490,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 119672092..0e9a52e14 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.multitenancy.TenantIdentifier; @@ -38,6 +37,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); @@ -51,12 +60,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()); @@ -92,6 +108,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) { @@ -103,12 +128,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; @@ -142,6 +173,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) { @@ -153,12 +193,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; @@ -191,11 +238,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; @@ -289,12 +341,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) @@ -307,16 +380,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"; @@ -331,6 +419,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); @@ -339,7 +432,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()); @@ -348,36 +441,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()); @@ -391,6 +530,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/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 b03ebd71c..bbcdb8dbf 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.*; @@ -346,4 +347,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 1b12646d4..d913919f6 100644 --- a/src/test/java/io/supertokens/test/multitenant/api/TestTenantIdIsNotPresentForOlderCDI.java +++ b/src/test/java/io/supertokens/test/multitenant/api/TestTenantIdIsNotPresentForOlderCDI.java @@ -121,7 +121,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -141,7 +141,7 @@ private void createTenants() new EmailPasswordConfig(true), new ThirdPartyConfig(true, null), new PasswordlessConfig(true), - config + null, null, config ) ); } @@ -161,7 +161,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 9c86c127e..0b2dcc3a7 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/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 7b13a4990..54c698899 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/PasswordlessConsumeCodeAPITest5_0.java b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java new file mode 100644 index 000000000..6488e8255 --- /dev/null +++ b/src/test/java/io/supertokens/test/passwordless/api/PasswordlessConsumeCodeAPITest5_0.java @@ -0,0 +1,1003 @@ +/* + * 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.assertEquals; +import static org.junit.Assert.assertNotNull; + +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)); + } + + @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/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + assertEquals(2, response.entrySet().size()); + assertEquals("OK", response.get("status").getAsString()); + + JsonObject consumedDevice = response.get("consumedDevice").getAsJsonObject(); + assertEquals("test@example.com", consumedDevice.get("email").getAsString()); + + 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 testConsumeCodeWithoutCreatingUsersReturnsUserIfItAlreadyExists() 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/consume", consumeCodeRequestBody, 1000, 1000, null, + SemVer.v5_0.get(), "passwordless"); + + checkResponse(response, false, email, null); + + int activeUsers = ActiveUsers.countUsersActiveSince(process.getProcess(), startTs); + assert (activeUsers == 1); + + 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/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); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + 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); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + 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); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + 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); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + 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); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + 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); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + 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); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + 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 + "==#"); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + 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 + "==#"); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", false); + + 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 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/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 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/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 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/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 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/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)); + } + + @Test + public void testLinkCodeWithCreateUserSetToTrue() 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); + consumeCodeRequestBody.addProperty("createRecipeUserIfNotExists", true); + + 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)); + } + + 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) { + assert (!userJson.has("email")); + } else { + assertEquals(email, userJson.get("emails").getAsJsonArray().get(0).getAsString()); + } + + if (phoneNumber == null) { + assert (!userJson.has("phoneNumber")); + } else if (phoneNumber != null) { + assertEquals(phoneNumber, userJson.get("phoneNumbers").getAsJsonArray().get(0).getAsString()); + } + + assertEquals(8, userJson.entrySet().size()); + assertEquals(response.get("recipeUserId").getAsString(), userJson.get("id").getAsString()); + + JsonObject consumedDevice = response.getAsJsonObject("consumedDevice"); + if (email != null) { + assertEquals(email, consumedDevice.get("email").getAsString()); + } + + if (phoneNumber != null) { + assertEquals(phoneNumber, consumedDevice.get("phoneNumber").getAsString()); + } + } +} 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 068ffb27a..fbdfe54b8 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..4afc4d279 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,83 @@ 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)); + + 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 3479cd320..d90b85010 100644 --- a/src/test/java/io/supertokens/test/totp/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/totp/api/MultitenantAPITest.java @@ -37,6 +37,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; @@ -74,7 +75,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)); @@ -107,6 +108,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -127,6 +129,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -147,6 +150,7 @@ private void createTenants() new EmailPasswordConfig(false), new ThirdPartyConfig(false, null), new PasswordlessConfig(true), + null, null, config ) ); @@ -249,9 +253,12 @@ public void testDevicesWorkAppWide() throws Exception { int userCount = 1; for (TenantIdentifier tenant1 : tenants) { createDevice(tenant1, "user" + userCount); + TOTPDevice device = Totp.getDevices(t1.withStorage(StorageLayer.getStorage(tenant1, process.getProcess())).toAppIdentifierWithStorage(), "user" + userCount)[0]; + String validTotp = TOTPRecipeTest.generateTotpCode(process.getProcess(), device); + verifyDevice(tenant1, "user" + userCount, validTotp); for (TenantIdentifier tenant2 : tenants) { - createDeviceAlreadyExists(tenant2, "user1"); + createDeviceAlreadyExists(tenant2, "user" + userCount); } userCount++; @@ -269,7 +276,7 @@ public void testSameCodeUsedOnDifferentTenantsIsAllowed() throws Exception { for (TenantIdentifier tenant1 : tenants) { JsonObject deviceResponse = createDevice(tenant1, "user" + userCount); String secretKey = deviceResponse.get("secret").getAsString(); - TOTPDevice device = new TOTPDevice("user" + userCount, "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, "user" + userCount, 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 7e135958c..f9e9b6dae 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, "deviceName", d1Secret, 30, 0, false); + TOTPDevice device1 = new TOTPDevice(externalUserId, "deviceName", d1Secret, 30, 1, 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, "deviceName", d2Secret, 30, 0, false); + TOTPDevice device2 = new TOTPDevice(externalUserId, "deviceName", d2Secret, 30, 1, 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/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 942653763..d36dfc1cb 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 ) ); }